From 12fd91a7a1ae5244ce6b651f7e3dddf5ad05b90f Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 28 Mar 2022 09:24:29 -0700 Subject: [PATCH 1/7] Add Microsoft.AspNetCore.OpenApi package Fix up schema type generation Post review polish Add new package to templates Split out SchemaGenerator Clean up usings --- AspNetCore.sln | 66 +- eng/Dependencies.props | 1 + eng/ProjectReferences.props | 1 + eng/Versions.props | 1 + .../src/Extensions/EndpointBuilder.cs | 5 + .../src/PublicAPI.Unshipped.txt | 2 + src/Http/HttpAbstractions.slnf | 10 +- .../src/Microsoft.AspNetCore.OpenApi.csproj | 25 + src/Http/OpenApi/src/OpenApiGenerator.cs | 472 ++++++++++ .../OpenApiRouteHandlerBuilderExtensions.cs | 85 ++ .../OpenApi/src/Properties/AssemblyInfo.cs | 6 + src/Http/OpenApi/src/PublicAPI.Shipped.txt | 1 + src/Http/OpenApi/src/PublicAPI.Unshipped.txt | 4 + src/Http/OpenApi/src/SchemaGenerator.cs | 44 + .../Microsoft.AspNetCore.OpenApi.Tests.csproj | 25 + .../OpenApi/test/OpenApiGeneratorTests.cs | 814 ++++++++++++++++++ .../Builder/EndpointRouteBuilderExtensions.cs | 1 + ...crosoft.DotNet.Web.ProjectTemplates.csproj | 1 + .../WebApi-CSharp.csproj.in | 1 + .../content/WebApi-CSharp/Program.cs | 3 + 20 files changed, 1549 insertions(+), 19 deletions(-) create mode 100644 src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj create mode 100644 src/Http/OpenApi/src/OpenApiGenerator.cs create mode 100644 src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs create mode 100644 src/Http/OpenApi/src/Properties/AssemblyInfo.cs create mode 100644 src/Http/OpenApi/src/PublicAPI.Shipped.txt create mode 100644 src/Http/OpenApi/src/PublicAPI.Unshipped.txt create mode 100644 src/Http/OpenApi/src/SchemaGenerator.cs create mode 100644 src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj create mode 100644 src/Http/OpenApi/test/OpenApiGeneratorTests.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index de3f6f05c9b7..4a5c1b153e6f 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1694,7 +1694,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\Http\Http.Results\tools\ResultsOfTGenerator\ResultsOfTGenerator.csproj", "{9716D0D0-2251-44DD-8596-67D253EEF41C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{85520C50-CF33-4A27-BEC9-272100870D9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2D2D1107-7389-473B-BDCE-BFA060EAC453}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E47D1385-64B3-429B-9B1D-B0D0B7B6E506}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{25642C23-0BB8-4FF7-9181-9599489679EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi", "src\Http\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\Http\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{77305727-1A53-402A-A4E8-4CFA0DBFACC6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -10137,22 +10147,38 @@ Global {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|arm64.ActiveCfg = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|arm64.Build.0 = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x64.ActiveCfg = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x64.Build.0 = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x86.ActiveCfg = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x86.Build.0 = Debug|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|Any CPU.Build.0 = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|arm64.ActiveCfg = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|arm64.Build.0 = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.ActiveCfg = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.Build.0 = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.ActiveCfg = Release|Any CPU - {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.Build.0 = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|arm64.ActiveCfg = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|arm64.Build.0 = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x64.Build.0 = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x86.Build.0 = Debug|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|Any CPU.Build.0 = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|arm64.ActiveCfg = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|arm64.Build.0 = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x64.ActiveCfg = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x64.Build.0 = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x86.ActiveCfg = Release|Any CPU + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x86.Build.0 = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|arm64.ActiveCfg = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|arm64.Build.0 = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x64.Build.0 = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x86.ActiveCfg = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x86.Build.0 = Debug|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|Any CPU.Build.0 = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|arm64.ActiveCfg = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|arm64.Build.0 = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x64.ActiveCfg = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x64.Build.0 = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x86.ActiveCfg = Release|Any CPU + {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -10992,7 +11018,11 @@ Global {B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} {489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} {8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D} - {9716D0D0-2251-44DD-8596-67D253EEF41C} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9} + {2D2D1107-7389-473B-BDCE-BFA060EAC453} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} + {E47D1385-64B3-429B-9B1D-B0D0B7B6E506} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} + {25642C23-0BB8-4FF7-9181-9599489679EB} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} + {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4} = {E47D1385-64B3-429B-9B1D-B0D0B7B6E506} + {77305727-1A53-402A-A4E8-4CFA0DBFACC6} = {25642C23-0BB8-4FF7-9181-9599489679EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 18be2fda9f68..0fbdbd667a71 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -62,6 +62,7 @@ and are generated based on the last package release. + diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 94a43b1c5331..0411bbd7c587 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -31,6 +31,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index d523303137d3..9990a35d9373 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -282,6 +282,7 @@ 2.4.3 4.0.1 6.0.0-preview.3.21167.1 + 1.2.3 diff --git a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs index e133399f4ad0..873a1dbe8bae 100644 --- a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs +++ b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs @@ -25,6 +25,11 @@ public abstract class EndpointBuilder /// public IList Metadata { get; } = new List(); + /// + /// Gets the associated with the endpoint. + /// + public IServiceProvider? ServiceProvider { get; set; } + /// /// Creates an instance of from the . /// diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index dc7700f1d4a5..4532909a61d7 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,7 @@ #nullable enable *REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! +Microsoft.AspNetCore.Builder.EndpointBuilder.ServiceProvider.get -> System.IServiceProvider? +Microsoft.AspNetCore.Builder.EndpointBuilder.ServiceProvider.set -> void Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 70fc8b56f6a8..f57c31fe5b62 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -26,6 +26,8 @@ "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Http\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj", + "src\\Http\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj", "src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj", "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", @@ -40,11 +42,16 @@ "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", - "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", + "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", + "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", + "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj", "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", + "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", + "src\\Mvc\\Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj", + "src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", @@ -52,6 +59,7 @@ "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj", "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" diff --git a/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj new file mode 100644 index 000000000000..b0d92ee2ff37 --- /dev/null +++ b/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj @@ -0,0 +1,25 @@ + + + + $(DefaultNetCoreTargetFramework) + true + aspnetcore;openapi + Provides APIs for annotating route handler endpoints in ASP.NET Core with OpenAPI annotations. + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Http/OpenApi/src/OpenApiGenerator.cs b/src/Http/OpenApi/src/OpenApiGenerator.cs new file mode 100644 index 000000000000..dbb4aa6b3355 --- /dev/null +++ b/src/Http/OpenApi/src/OpenApiGenerator.cs @@ -0,0 +1,472 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.OpenApi.Models; + +namespace Microsoft.AspNetCore.OpenApi; + +/// +/// Defines a set of methods for generating OpenAPI definitions for endpoints. +/// +internal class OpenApiGenerator +{ + private readonly IHostEnvironment? _environment; + private readonly IServiceProviderIsService? _serviceProviderIsService; + private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); + + /// + /// Creates an instance given a + /// and a instance. + /// + /// The host environment. + /// The service to determine if the a type is available from the . + internal OpenApiGenerator( + IHostEnvironment? environment, + IServiceProviderIsService? serviceProviderIsService) + { + _environment = environment; + _serviceProviderIsService = serviceProviderIsService; + } + + /// + /// Generates an for a given . + /// + /// The associated with the route handler of the endpoint. + /// The endpoint . + /// The route pattern. + /// An annotation derived from the given inputs. + internal OpenApiOperation? GetOpenApiOperation( + MethodInfo methodInfo, + EndpointMetadataCollection metadata, + RoutePattern pattern) + { + if (metadata.GetMetadata() is { } httpMethodMetadata && + httpMethodMetadata.HttpMethods.SingleOrDefault() is { } method && + metadata.GetMetadata() is null or { ExcludeFromDescription: false }) + { + return GetOperation(method, methodInfo, metadata, pattern); + } + + return null; + } + + private OpenApiOperation GetOperation(string httpMethod, MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern) + { + var disableInferredBody = ShouldDisableInferredBody(httpMethod); + return new OpenApiOperation + { + OperationId = metadata.GetMetadata()?.EndpointName, + Summary = metadata.GetMetadata()?.Summary, + Description = metadata.GetMetadata()?.Description, + Tags = GetOperationTags(methodInfo, metadata), + Parameters = GetOpenApiParameters(methodInfo, metadata, pattern, disableInferredBody), + RequestBody = GetOpenApiRequestBody(methodInfo, metadata, pattern), + Responses = GetOpenApiResponses(methodInfo, metadata) + }; + + static bool ShouldDisableInferredBody(string method) + { + // GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies + return method.Equals(HttpMethods.Get, StringComparison.Ordinal) || + method.Equals(HttpMethods.Delete, StringComparison.Ordinal) || + method.Equals(HttpMethods.Head, StringComparison.Ordinal) || + method.Equals(HttpMethods.Options, StringComparison.Ordinal) || + method.Equals(HttpMethods.Trace, StringComparison.Ordinal) || + method.Equals(HttpMethods.Connect, StringComparison.Ordinal); + } + } + + private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointMetadataCollection metadata) + { + var responses = new OpenApiResponses(); + var responseType = method.ReturnType; + if (AwaitableInfo.IsTypeAwaitable(responseType, out var awaitableInfo)) + { + responseType = awaitableInfo.ResultType; + } + + if (typeof(IResult).IsAssignableFrom(responseType)) + { + responseType = typeof(void); + } + + var errorMetadata = metadata.GetMetadata(); + var defaultErrorType = errorMetadata?.Type; + + var responseProviderMetadata = metadata.GetOrderedMetadata(); + var producesResponseMetadata = metadata.GetOrderedMetadata(); + + var eligibileAnnotations = new Dictionary(); + + foreach (var responseMetadata in producesResponseMetadata) + { + var statusCode = responseMetadata.StatusCode; + + var discoveredTypeAnnotation = responseMetadata.Type; + var discoveredContentTypeAnnotation = new MediaTypeCollection(); + + if (discoveredTypeAnnotation == typeof(void)) + { + if (responseType != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created)) + { + discoveredTypeAnnotation = responseType; + } + } + + foreach (var contentType in responseMetadata.ContentTypes) + { + discoveredContentTypeAnnotation.Add(contentType); + } + + discoveredTypeAnnotation = discoveredTypeAnnotation == null || discoveredTypeAnnotation == typeof(void) + ? responseType + : discoveredTypeAnnotation; + + if (discoveredTypeAnnotation is not null) + { + GenerateDefaultContent(discoveredContentTypeAnnotation, discoveredTypeAnnotation); + eligibileAnnotations.Add(statusCode, (discoveredTypeAnnotation, discoveredContentTypeAnnotation)); + } + } + + foreach (var providerMetadata in responseProviderMetadata) + { + var statusCode = providerMetadata.StatusCode; + + var discoveredTypeAnnotation = providerMetadata.Type; + var discoveredContentTypeAnnotation = new MediaTypeCollection(); + + if (discoveredTypeAnnotation == typeof(void)) + { + if (responseType != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created)) + { + // ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified. + // In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a + // [ProducesResponseType(201)] instead of [ProducesResponseType(typeof(Person), 201] when typeof(Person) can be inferred + // from the return type. + discoveredTypeAnnotation = responseType; + } + else if (statusCode >= 400 && statusCode < 500) + { + // Determine whether or not the type was provided by the user. If so, favor it over the default + // error type for 4xx client errors if no response type is specified. + discoveredTypeAnnotation = defaultErrorType is not null ? defaultErrorType : discoveredTypeAnnotation; + } + else if (providerMetadata is IApiDefaultResponseMetadataProvider) + { + discoveredTypeAnnotation = defaultErrorType; + } + } + + providerMetadata.SetContentTypes(discoveredContentTypeAnnotation); + + discoveredTypeAnnotation = discoveredTypeAnnotation == null || discoveredTypeAnnotation == typeof(void) + ? responseType + : discoveredTypeAnnotation; + + GenerateDefaultContent(discoveredContentTypeAnnotation, discoveredTypeAnnotation); + eligibileAnnotations.Add(statusCode, (discoveredTypeAnnotation, discoveredContentTypeAnnotation)); + } + + if (eligibileAnnotations.Count == 0) + { + GenerateDefaultResponses(eligibileAnnotations, responseType); + } + + foreach (var annotation in eligibileAnnotations) + { + var statusCode = $"{annotation.Key}"; + var (type, contentTypes) = annotation.Value; + var responseContent = new Dictionary(); + + foreach (var contentType in contentTypes) + { + responseContent[contentType] = new OpenApiMediaType + { + Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(type) } + }; + } + + responses[statusCode] = new OpenApiResponse { Content = responseContent }; + } + + return responses; + } + + private static void GenerateDefaultContent(MediaTypeCollection discoveredContentTypeAnnotation, Type? discoveredTypeAnnotation) + { + if (discoveredContentTypeAnnotation.Count == 0) + { + if (discoveredTypeAnnotation == typeof(void) || discoveredTypeAnnotation == null) + { + return; + } + if (discoveredTypeAnnotation == typeof(string)) + { + discoveredContentTypeAnnotation.Add("text/plain"); + } + else + { + discoveredContentTypeAnnotation.Add("application/json"); + } + } + } + + private static void GenerateDefaultResponses(Dictionary eligibleAnnotations, Type responseType) + { + if (responseType == typeof(void)) + { + eligibleAnnotations.Add(StatusCodes.Status200OK, (responseType, new MediaTypeCollection())); + } + else if (responseType == typeof(string)) + { + eligibleAnnotations.Add(StatusCodes.Status200OK, (responseType, new MediaTypeCollection() { "text/plain" })); + } + else + { + eligibleAnnotations.Add(StatusCodes.Status200OK, (responseType, new MediaTypeCollection() { "application/json" })); + } + } + + private OpenApiRequestBody? GetOpenApiRequestBody(MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern) + { + var hasFormOrBodyParameter = false; + ParameterInfo? requestBodyParameter = null; + + foreach (var parameter in methodInfo.GetParameters()) + { + var (bodyOrFormParameter, _) = GetOpenApiParameterLocation(parameter, pattern, false); + hasFormOrBodyParameter |= bodyOrFormParameter; + if (hasFormOrBodyParameter) + { + requestBodyParameter = parameter; + break; + } + } + + var acceptsMetadata = metadata.GetMetadata(); + var requestBodyContent = new Dictionary(); + var isRequired = false; + + if (acceptsMetadata is not null) + { + foreach (var contentType in acceptsMetadata.ContentTypes) + { + requestBodyContent[contentType] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = SchemaGenerator.GetOpenApiSchemaType(acceptsMetadata.RequestType ?? requestBodyParameter?.ParameterType) + } + }; + } + isRequired = !acceptsMetadata.IsOptional; + } + + if (!hasFormOrBodyParameter) + { + return new OpenApiRequestBody() + { + Required = isRequired, + Content = requestBodyContent + }; + } + + if (requestBodyParameter is not null) + { + if (requestBodyContent.Count == 0) + { + var isFormType = requestBodyParameter.ParameterType == typeof(IFormFile) || requestBodyParameter.ParameterType == typeof(IFormFileCollection); + var hasFormAttribute = requestBodyParameter.GetCustomAttributes().OfType().FirstOrDefault() != null; + if (isFormType || hasFormAttribute) + { + requestBodyContent["multipart/form-data"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType) + } + }; + } + else + { + requestBodyContent["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType) + } + }; ; + } + } + + var nullabilityContext = new NullabilityInfoContext(); + var nullability = nullabilityContext.Create(requestBodyParameter); + var allowEmpty = requestBodyParameter.GetCustomAttributes().OfType().SingleOrDefault()?.AllowEmpty ?? false; + var isOptional = requestBodyParameter.HasDefaultValue + || nullability.ReadState != NullabilityState.NotNull + || allowEmpty; + + return new OpenApiRequestBody + { + Required = !isOptional, + Content = requestBodyContent + }; + } + + return null; + } + + private IList GetOperationTags(MethodInfo methodInfo, EndpointMetadataCollection metadata) + { + var tags = metadata.GetMetadata(); + string controllerName; + + if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGeneratedType(methodInfo.DeclaringType)) + { + controllerName = methodInfo.DeclaringType.Name; + } + else + { + // If the declaring type is null or compiler-generated (e.g. lambdas), + // group the methods under the application name. + controllerName = _environment?.ApplicationName ?? string.Empty; + } + + return tags is not null + ? tags.Tags.Select(tag => new OpenApiTag() { Name = tag }).ToList() + : new List() { new OpenApiTag() { Name = controllerName } }; + } + + private IList GetOpenApiParameters(MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern, bool disableInferredBody) + { + var parameters = methodInfo.GetParameters(); + var openApiParameters = new List(); + + foreach (var parameter in parameters) + { + var (isBodyOrFormParameter, parameterLocation) = GetOpenApiParameterLocation(parameter, pattern, disableInferredBody); + + // If the parameter isn't something that would be populated in RequestBody + // or doesn't have a valid ParameterLocation, then it must be a service + // parameter that we can ignore. + if (!isBodyOrFormParameter && parameterLocation is null) + { + continue; + } + + var nullabilityContext = new NullabilityInfoContext(); + var nullability = nullabilityContext.Create(parameter); + var isOptional = parameter.HasDefaultValue || nullability.ReadState != NullabilityState.NotNull; + var openApiParameter = new OpenApiParameter() + { + Name = parameter.Name, + In = parameterLocation, + Content = GetOpenApiParameterContent(metadata), + Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(parameter.ParameterType) }, + Required = !isOptional + + }; + openApiParameters.Add(openApiParameter); + } + + return openApiParameters; + } + + private static IDictionary GetOpenApiParameterContent(EndpointMetadataCollection metadata) + { + var openApiParameterContent = new Dictionary(); + var acceptsMetadata = metadata.GetMetadata(); + if (acceptsMetadata is not null) + { + foreach (var contentType in acceptsMetadata.ContentTypes) + { + openApiParameterContent.Add(contentType, new OpenApiMediaType()); + } + } + + return openApiParameterContent; + } + + private (bool, ParameterLocation?) GetOpenApiParameterLocation(ParameterInfo parameter, RoutePattern pattern, bool disableInferredBody) + { + var attributes = parameter.GetCustomAttributes(); + + if (attributes.OfType().FirstOrDefault() is { } routeAttribute) + { + return (false, ParameterLocation.Path); + } + else if (attributes.OfType().FirstOrDefault() is { } queryAttribute) + { + return (false, ParameterLocation.Query); + } + else if (attributes.OfType().FirstOrDefault() is { } headerAttribute) + { + return (false, ParameterLocation.Header); + } + else if (attributes.OfType().FirstOrDefault() is { } fromBodyAttribute) + { + return (true, null); + } + else if (attributes.OfType().FirstOrDefault() is { } fromFormAttribute) + { + return (true, null); + } + else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) || + parameter.ParameterType == typeof(HttpContext) || + parameter.ParameterType == typeof(HttpRequest) || + parameter.ParameterType == typeof(HttpResponse) || + parameter.ParameterType == typeof(ClaimsPrincipal) || + parameter.ParameterType == typeof(CancellationToken) || + ParameterBindingMethodCache.HasBindAsyncMethod(parameter) || + _serviceProviderIsService?.IsService(parameter.ParameterType) == true) + { + return (false, null); + } + else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType)) + { + // complex types will display as strings since they use custom parsing via TryParse on a string + var displayType = !parameter.ParameterType.IsPrimitive && Nullable.GetUnderlyingType(parameter.ParameterType)?.IsPrimitive != true + ? typeof(string) : parameter.ParameterType; + // Path vs query cannot be determined by RequestDelegateFactory at startup currently because of the layering, but can be done here. + if (parameter.Name is { } name && pattern.GetParameter(name) is not null) + { + return (false, ParameterLocation.Path); + } + else + { + return (false, ParameterLocation.Query); + } + } + else if (parameter.ParameterType == typeof(IFormFile) || parameter.ParameterType == typeof(IFormFileCollection)) + { + return (true, null); + } + else if (disableInferredBody && ( + (parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!)) || + parameter.ParameterType == typeof(string[]) || + parameter.ParameterType == typeof(StringValues))) + { + return (false, ParameterLocation.Query); + } + else + { + return (true, null); + } + } +} diff --git a/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs new file mode 100644 index 000000000000..3be3b3c3fe9e --- /dev/null +++ b/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace Microsoft.AspNetCore.OpenApi; + +/// +/// Extension methods for annotating OpenAPI descriptions on an . +/// +public static class OpenApiRouteHandlerBuilderExtensions +{ + /// + /// Adds an OpenAPI annotation to associated + /// with the current endpoint. + /// + /// The . + /// A that can be used to further customize the endpoint. + + public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder) + { + builder.Add(endpointBuilder => + { + if (endpointBuilder is RouteEndpointBuilder routeEndpointBuilder) + { + var openApiOperation = GetOperationForEndpoint(routeEndpointBuilder); + if (openApiOperation != null) + { + routeEndpointBuilder.Metadata.Add(openApiOperation); + } + }; + }); + return builder; + + } + + /// + /// Adds an OpenAPI annotation to associated + /// with the current endpoint and modifies it with the given . + /// + /// The . + /// An that mutates an OpenAPI annotation. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder, Func configureOperation) + { + builder.Add(endpointBuilder => + { + if (endpointBuilder is RouteEndpointBuilder routeEndpointBuilder) + { + var openApiOperation = GetOperationForEndpoint(routeEndpointBuilder); + if (openApiOperation != null) + { + routeEndpointBuilder.Metadata.Add(configureOperation(openApiOperation)); + } + + }; + }); + return builder; + } + + private static OpenApiOperation? GetOperationForEndpoint(RouteEndpointBuilder routeEndpointBuilder) + { + var pattern = routeEndpointBuilder.RoutePattern; + var metadata = new EndpointMetadataCollection(routeEndpointBuilder.Metadata); + var methodInfo = metadata.OfType().SingleOrDefault(); + var serviceProvider = routeEndpointBuilder.ServiceProvider; + + if (methodInfo == null || serviceProvider == null) + { + return null; + } + + var hostEnvironment = serviceProvider.GetService(); + var serviceProviderIsService = serviceProvider.GetService(); + var generator = new OpenApiGenerator(hostEnvironment, serviceProviderIsService); + return generator.GetOpenApiOperation(methodInfo, metadata, pattern); + } +} diff --git a/src/Http/OpenApi/src/Properties/AssemblyInfo.cs b/src/Http/OpenApi/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..d500965b263d --- /dev/null +++ b/src/Http/OpenApi/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/OpenApi/src/PublicAPI.Shipped.txt b/src/Http/OpenApi/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/Http/OpenApi/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Http/OpenApi/src/PublicAPI.Unshipped.txt b/src/Http/OpenApi/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..d83debd1c5d6 --- /dev/null +++ b/src/Http/OpenApi/src/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +#nullable enable +Microsoft.AspNetCore.OpenApi.OpenApiRouteHandlerBuilderExtensions +static Microsoft.AspNetCore.OpenApi.OpenApiRouteHandlerBuilderExtensions.WithOpenApi(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.OpenApi.OpenApiRouteHandlerBuilderExtensions.WithOpenApi(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! configureOperation) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! diff --git a/src/Http/OpenApi/src/SchemaGenerator.cs b/src/Http/OpenApi/src/SchemaGenerator.cs new file mode 100644 index 000000000000..3eeb16fe4e79 --- /dev/null +++ b/src/Http/OpenApi/src/SchemaGenerator.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OpenApi; + +internal static class SchemaGenerator +{ + internal static string GetOpenApiSchemaType(Type? inputType) + { + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + var type = Nullable.GetUnderlyingType(inputType) ?? inputType; + + if (typeof(string).IsAssignableFrom(type) || typeof(DateTime).IsAssignableTo(type)) + { + return "string"; + } + else if (typeof(bool).IsAssignableFrom(type)) + { + return "boolean"; + } + else if (typeof(int).IsAssignableFrom(type) + || typeof(double).IsAssignableFrom(type) + || typeof(float).IsAssignableFrom(type)) + { + return "number"; + } + else if (typeof(long).IsAssignableFrom(type)) + { + return "integer"; + } + else if (type.IsArray) + { + return "array"; + } + else + { + return "object"; + } + } +} diff --git a/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj new file mode 100644 index 000000000000..4641ccd2f83d --- /dev/null +++ b/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj @@ -0,0 +1,25 @@ + + + + $(DefaultNetCoreTargetFramework) + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/Http/OpenApi/test/OpenApiGeneratorTests.cs b/src/Http/OpenApi/test/OpenApiGeneratorTests.cs new file mode 100644 index 000000000000..7a67e11b020d --- /dev/null +++ b/src/Http/OpenApi/test/OpenApiGeneratorTests.cs @@ -0,0 +1,814 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using Microsoft.OpenApi.Models; + +namespace Microsoft.AspNetCore.OpenApi.Tests; + +public class OpenApiOperationGeneratorTests +{ + [Fact] + public void OperationNotCreatedIfNoHttpMethods() + { + var operation = GetOpenApiOperation(() => { }, "/", Array.Empty()); + + Assert.Null(operation); + } + + [Fact] + public void UsesDeclaringTypeAsOperationTags() + { + var operation = GetOpenApiOperation(TestAction); + + var declaringTypeName = typeof(OpenApiOperationGeneratorTests).Name; + var tag = Assert.Single(operation.Tags); + + Assert.Equal(declaringTypeName, tag.Name); + + } + + [Fact] + public void UsesApplicationNameAsOperationTagsIfNoDeclaringType() + { + var operation = GetOpenApiOperation(() => { }); + + var declaringTypeName = nameof(OpenApiOperationGeneratorTests); + var tag = Assert.Single(operation.Tags); + + Assert.Equal(declaringTypeName, tag.Name); + } + + [Fact] + public void AddsRequestFormatFromMetadata() + { + static void AssertCustomRequestFormat(OpenApiOperation operation) + { + var request = Assert.Single(operation.Parameters); + var content = Assert.Single(request.Content); + Assert.Equal("application/custom", content.Key); + } + + AssertCustomRequestFormat(GetOpenApiOperation( + [Consumes("application/custom")] + (InferredJsonClass fromBody) => + { })); + + AssertCustomRequestFormat(GetOpenApiOperation( + [Consumes("application/custom")] + ([FromBody] int fromBody) => + { })); + } + + [Fact] + public void AddsMultipleRequestFormatsFromMetadata() + { + var operation = GetOpenApiOperation( + [Consumes("application/custom0", "application/custom1")] + (InferredJsonClass fromBody) => + { }); + + var request = Assert.Single(operation.Parameters); + + Assert.Equal(2, request.Content.Count); + Assert.Equal(new[] { "application/custom0", "application/custom1" }, request.Content.Keys); + } + + [Fact] + public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter() + { + var operation = GetOpenApiOperation( + [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] + () => + { }); ; + var request = operation.RequestBody; + Assert.NotNull(request); + + Assert.Equal(2, request.Content.Count); + + Assert.Equal("object", request.Content.First().Value.Schema.Type); + Assert.Equal("object", request.Content.Last().Value.Schema.Type); + Assert.False(request.Required); + } + +#nullable enable + + [Fact] + public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter() + { + var operation = GetOpenApiOperation( + [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] + (InferredJsonClass fromBody) => + { }); + + var request = operation.RequestBody; + Assert.NotNull(request); + + Assert.Equal("object", request.Content.First().Value.Schema.Type); + Assert.True(request.Required); + } + +#nullable disable + + [Fact] + public void AddsJsonResponseFormatWhenFromBodyInferred() + { + static void AssertJsonResponse(OpenApiOperation operation, string expectedType) + { + var response = Assert.Single(operation.Responses); + Assert.Equal("200", response.Key); + var formats = Assert.Single(response.Value.Content); + Assert.Equal(expectedType, formats.Value.Schema.Type); + + Assert.Equal("application/json", formats.Key); + } + + AssertJsonResponse(GetOpenApiOperation(() => new InferredJsonClass()), "object"); + AssertJsonResponse(GetOpenApiOperation(() => (IInferredJsonInterface)null), "object"); + } + + [Fact] + public void AddsTextResponseFormatWhenFromBodyInferred() + { + var operation = GetOpenApiOperation(() => "foo"); + + var response = Assert.Single(operation.Responses); + Assert.Equal("200", response.Key); + var formats = Assert.Single(response.Value.Content); + Assert.Equal("string", formats.Value.Schema.Type); + Assert.Equal("text/plain", formats.Key); + } + + [Fact] + public void AddsNoResponseFormatWhenItCannotBeInferredAndTheresNoMetadata() + { + static void AssertVoid(OpenApiOperation operation) + { + ; + var response = Assert.Single(operation.Responses); + Assert.Equal("200", response.Key); + Assert.Empty(response.Value.Content); + } + + AssertVoid(GetOpenApiOperation(() => { })); + AssertVoid(GetOpenApiOperation(() => Task.CompletedTask)); + AssertVoid(GetOpenApiOperation(() => new ValueTask())); + } + + [Fact] + public void AddsMultipleResponseFormatsFromMetadataWithPoco() + { + var operation = GetOpenApiOperation( + [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + () => new InferredJsonClass()); + + var responses = operation.Responses; + + Assert.Equal(2, responses.Count); + + var createdResponseType = responses["201"]; + var content = Assert.Single(createdResponseType.Content); + + Assert.NotNull(createdResponseType); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("application/json", createdResponseType.Content.Keys.First()); + + var badRequestResponseType = responses["400"]; + + Assert.NotNull(badRequestResponseType); + Assert.Equal("object", badRequestResponseType.Content.Values.First().Schema.Type); + Assert.Equal("application/json", badRequestResponseType.Content.Keys.First()); + } + + [Fact] + public void AddsMultipleResponseFormatsFromMetadataWithIResult() + { + var operation = GetOpenApiOperation( + [ProducesResponseType(typeof(InferredJsonClass), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + () => Results.Ok(new InferredJsonClass())); + + Assert.Equal(2, operation.Responses.Count); + + var createdResponseType = operation.Responses["201"]; + var createdResponseContent = Assert.Single(createdResponseType.Content); + + Assert.NotNull(createdResponseType); + Assert.Equal("object", createdResponseContent.Value.Schema.Type); + Assert.Equal("application/json", createdResponseContent.Key); + + var badRequestResponseType = operation.Responses["400"]; + + Assert.NotNull(badRequestResponseType); + Assert.Empty(badRequestResponseType.Content); + } + + [Fact] + public void AddsFromRouteParameterAsPath() + { + static void AssertPathParameter(OpenApiOperation operation) + { + var param = Assert.Single(operation.Parameters); + Assert.Equal("number", param.Schema.Type); + Assert.Equal(ParameterLocation.Path, param.In); + } + + AssertPathParameter(GetOpenApiOperation((int foo) => { }, "/{foo}")); + AssertPathParameter(GetOpenApiOperation(([FromRoute] int foo) => { })); + } + + [Fact] + public void AddsFromRouteParameterAsPathWithCustomClassWithTryParse() + { + static void AssertPathParameter(OpenApiOperation operation) + { + var param = Assert.Single(operation.Parameters); + Assert.Equal("object", param.Schema.Type); + Assert.Equal(ParameterLocation.Path, param.In); + } + AssertPathParameter(GetOpenApiOperation((TryParseStringRecord foo) => { }, pattern: "/{foo}")); + } + + [Fact] + public void AddsFromRouteParameterAsPathWithNullablePrimitiveType() + { + static void AssertPathParameter(OpenApiOperation operation) + { + var param = Assert.Single(operation.Parameters); + Assert.Equal("number", param.Schema.Type); + Assert.Equal(ParameterLocation.Path, param.In); + } + + AssertPathParameter(GetOpenApiOperation((int? foo) => { }, "/{foo}")); + AssertPathParameter(GetOpenApiOperation(([FromRoute] int? foo) => { })); + } + + [Fact] + public void AddsFromRouteParameterAsPathWithStructTypeWithTryParse() + { + static void AssertPathParameter(OpenApiOperation operation) + { + var param = Assert.Single(operation.Parameters); + Assert.Equal("object", param.Schema.Type); + Assert.Equal(ParameterLocation.Path, param.In); + } + AssertPathParameter(GetOpenApiOperation((TryParseStringRecordStruct foo) => { }, pattern: "/{foo}")); + } + + [Fact] + public void AddsFromQueryParameterAsQuery() + { + static void AssertQueryParameter(OpenApiOperation operation, string type) + { + var param = Assert.Single(operation.Parameters); ; + Assert.Equal(type, param.Schema.Type); + Assert.Equal(ParameterLocation.Query, param.In); + } + + AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), "number"); + AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), "number"); + AssertQueryParameter(GetOpenApiOperation(([FromQuery] TryParseStringRecordStruct foo) => { }), "object"); + AssertQueryParameter(GetOpenApiOperation((int[] foo) => { }, "/"), "array"); + AssertQueryParameter(GetOpenApiOperation((string[] foo) => { }, "/"), "array"); + AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), "object"); + AssertQueryParameter(GetOpenApiOperation((TryParseStringRecordStruct[] foo) => { }, "/"), "array"); + } + + [Theory] + [InlineData("Put")] + [InlineData("Post")] + public void BodyIsInferredForArraysInsteadOfQuerySomeHttpMethods(string httpMethod) + { + static void AssertBody(OpenApiOperation operation, string expectedType) + { + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal(expectedType, content.Value.Schema.Type); + } + + AssertBody(GetOpenApiOperation((int[] foo) => { }, "/", httpMethods: new[] { httpMethod }), "array"); + AssertBody(GetOpenApiOperation((string[] foo) => { }, "/", httpMethods: new[] { httpMethod }), "array"); + AssertBody(GetOpenApiOperation((TryParseStringRecordStruct[] foo) => { }, "/", httpMethods: new[] { httpMethod }), "array"); + } + + [Fact] + public void AddsFromHeaderParameterAsHeader() + { + var operation = GetOpenApiOperation(([FromHeader] int foo) => { }); + var param = Assert.Single(operation.Parameters); + + Assert.Equal("number", param.Schema.Type); + Assert.Equal(ParameterLocation.Header, param.In); + } + + [Fact] + public void DoesNotAddFromServiceParameterAsService() + { + Assert.Empty(GetOpenApiOperation((IInferredServiceInterface foo) => { }).Parameters); + Assert.Empty(GetOpenApiOperation(([FromServices] int foo) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((HttpContext context) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((HttpRequest request) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((HttpResponse response) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((ClaimsPrincipal user) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((CancellationToken token) => { }).Parameters); + Assert.Empty(GetOpenApiOperation((BindAsyncRecord context) => { }).Parameters); + } + + [Fact] + public void AddsBodyParameterInTheParameterDescription() + { + static void AssertBodyParameter(OpenApiOperation operation, string expectedName, string expectedType) + { + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal(expectedType, content.Value.Schema.Type); + } + + AssertBodyParameter(GetOpenApiOperation((InferredJsonClass foo) => { }), "foo", "object"); + AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", "number"); + } + +#nullable enable + + [Fact] + public void AddsMultipleParameters() + { + var operation = GetOpenApiOperation(([FromRoute] int foo, int bar, InferredJsonClass fromBody) => { }); + Assert.Equal(3, operation.Parameters.Count); + + var fooParam = operation.Parameters[0]; + Assert.Equal("foo", fooParam.Name); + Assert.Equal("number", fooParam.Schema.Type); + Assert.Equal(ParameterLocation.Path, fooParam.In); + Assert.True(fooParam.Required); + + var barParam = operation.Parameters[1]; + Assert.Equal("bar", barParam.Name); + Assert.Equal("number", barParam.Schema.Type); + Assert.Equal(ParameterLocation.Query, barParam.In); + Assert.True(barParam.Required); + + var fromBodyParam = operation.RequestBody; + Assert.Equal("object", fromBodyParam.Content.First().Value.Schema.Type); + Assert.True(fromBodyParam.Required); + } + +#nullable disable + + [Fact] + public void TestParameterIsRequired() + { + var operation = GetOpenApiOperation(([FromRoute] int foo, int? bar) => { }); + Assert.Equal(2, operation.Parameters.Count); + + var fooParam = operation.Parameters[0]; + Assert.Equal("foo", fooParam.Name); + Assert.Equal("number", fooParam.Schema.Type); + Assert.Equal(ParameterLocation.Path, fooParam.In); + Assert.True(fooParam.Required); + + var barParam = operation.Parameters[1]; + Assert.Equal("bar", barParam.Name); + Assert.Equal("number", barParam.Schema.Type); + Assert.Equal(ParameterLocation.Query, barParam.In); + Assert.False(barParam.Required); + } + + [Fact] + public void TestParameterIsRequiredForObliviousNullabilityContext() + { + // In an oblivious nullability context, reference type parameters without + // annotations are optional. Value type parameters are always required. + var operation = GetOpenApiOperation((string foo, int bar) => { }); + Assert.Equal(2, operation.Parameters.Count); + + var fooParam = operation.Parameters[0]; + Assert.Equal("string", fooParam.Schema.Type); + Assert.Equal(ParameterLocation.Query, fooParam.In); + Assert.False(fooParam.Required); + + var barParam = operation.Parameters[1]; + Assert.Equal("number", barParam.Schema.Type); + Assert.Equal(ParameterLocation.Query, barParam.In); + Assert.True(barParam.Required); + } + + [Fact] + public void RespectProducesProblemMetadata() + { + // Arrange + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new[] { + new ProducesResponseTypeMetadata(typeof(ProblemDetails), StatusCodes.Status400BadRequest, "application/json+problem") }); + + // Assert + var responses = Assert.Single(operation.Responses); + var content = Assert.Single(responses.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + } + + [Fact] + public void RespectsProducesWithGroupNameExtensionMethod() + { + // Arrange + var endpointGroupName = "SomeEndpointGroupName"; + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new object[] + { + new ProducesResponseTypeMetadata(typeof(InferredJsonClass), StatusCodes.Status200OK, "application/json"), + new EndpointNameMetadata(endpointGroupName) + }); + + var responses = Assert.Single(operation.Responses); + var content = Assert.Single(responses.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + } + + [Fact] + public void RespectsExcludeFromDescription() + { + // Arrange + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new object[] + { + new ProducesResponseTypeMetadata(typeof(InferredJsonClass), StatusCodes.Status200OK, "application/json"), + new ExcludeFromDescriptionAttribute() + }); + + Assert.Null(operation); + } + + [Fact] + public void HandlesProducesWithProducesProblem() + { + // Arrange + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new[] + { + new ProducesResponseTypeMetadata(typeof(InferredJsonClass), StatusCodes.Status200OK, "application/json"), + new ProducesResponseTypeMetadata(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json"), + new ProducesResponseTypeMetadata(typeof(ProblemDetails), StatusCodes.Status404NotFound, "application/problem+json"), + new ProducesResponseTypeMetadata(typeof(ProblemDetails), StatusCodes.Status409Conflict, "application/problem+json") + }); + var responses = operation.Responses; + + // Assert + Assert.Collection( + responses.OrderBy(response => response.Key), + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("200", responseType.Key); + Assert.Equal("application/json", content.Key); + }, + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("400", responseType.Key); + Assert.Equal("application/problem+json", content.Key); + }, + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("404", responseType.Key); + Assert.Equal("application/problem+json", content.Key); + }, + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("409", responseType.Key); + Assert.Equal("application/problem+json", content.Key); + }); + } + + [Fact] + public void HandleMultipleProduces() + { + // Arrange + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new[] + { + new ProducesResponseTypeMetadata(typeof(InferredJsonClass), StatusCodes.Status200OK, "application/json"), + new ProducesResponseTypeMetadata(typeof(InferredJsonClass), StatusCodes.Status201Created, "application/json") + }); + + var responses = operation.Responses; + + // Assert + Assert.Collection( + responses.OrderBy(response => response.Key), + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("200", responseType.Key); + Assert.Equal("application/json", content.Key); + }, + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("201", responseType.Key); + Assert.Equal("application/json", content.Key); + }); + } + + [Fact] + public void HandleAcceptsMetadata() + { + // Arrange + var operation = GetOpenApiOperation(() => "", + additionalMetadata: new[] + { + new AcceptsMetadata(typeof(string), true, new string[] { "application/json", "application/xml"}) + }); + + var requestBody = operation.RequestBody; + + // Assert + Assert.Collection( + requestBody.Content, + parameter => + { + Assert.Equal("application/json", parameter.Key); + }, + parameter => + { + Assert.Equal("application/xml", parameter.Key); + }); + } + + [Fact] + public void HandleAcceptsMetadataWithTypeParameter() + { + // Arrange + var operation = GetOpenApiOperation((InferredJsonClass inferredJsonClass) => "", + additionalMetadata: new[] + { + new AcceptsMetadata(typeof(InferredJsonClass), true, new string[] { "application/json"}) + }); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.False(requestBody.Required); + } + +#nullable enable + + [Fact] + public void HandleDefaultIAcceptsMetadataForRequiredBodyParameter() + { + // Arrange + var operation = GetOpenApiOperation((InferredJsonClass inferredJsonClass) => ""); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("application/json", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.True(requestBody.Required); + } + + [Fact] + public void HandleDefaultIAcceptsMetadataForOptionalBodyParameter() + { + // Arrange + var operation = GetOpenApiOperation((InferredJsonClass? inferredJsonClass) => ""); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("application/json", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.False(requestBody.Required); + } + + [Fact] + public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBodyType() + { + // Arrange + var operation = GetOpenApiOperation([Consumes("application/xml")] (InferredJsonClass? inferredJsonClass) => ""); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("application/xml", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.False(requestBody.Required); + } + + [Fact] + public void HandleDefaultIAcceptsMetadataForRequiredFormFileParameter() + { + // Arrange + var operation = GetOpenApiOperation((IFormFile inferredFormFile) => ""); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("multipart/form-data", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.True(requestBody.Required); + } + + [Fact] + public void HandleDefaultIAcceptsMetadataForOptionalFormFileParameter() + { + // Arrange + var operation = GetOpenApiOperation((IFormFile? inferredFormFile) => ""); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("multipart/form-data", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.False(requestBody.Required); + } + + [Fact] + public void AddsMultipartFormDataRequestFormatWhenFormFileSpecified() + { + // Arrange + var operation = GetOpenApiOperation((IFormFile file) => Results.NoContent()); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("multipart/form-data", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.True(requestBody.Required); + } + + [Fact] + public void HasMultipleRequestFormatsWhenFormFileSpecifiedWithConsumesAttribute() + { + var operation = GetOpenApiOperation( + [Consumes("application/custom0", "application/custom1")] (IFormFile file) => Results.NoContent()); + + var requestBody = operation.RequestBody; + var content = requestBody.Content; + + Assert.Equal(2, content.Count); + + var requestFormat0 = content["application/custom0"]; + Assert.NotNull(requestFormat0); + + var requestFormat1 = content["application/custom1"]; + Assert.NotNull(requestFormat1); + } + + [Fact] + public void TestIsRequiredFromFormFile() + { + var operation0 = GetOpenApiOperation((IFormFile fromFile) => { }); + var operation1 = GetOpenApiOperation((IFormFile? fromFile) => { }); + Assert.NotNull(operation0.RequestBody); + Assert.NotNull(operation1.RequestBody); + + var fromFileParam0 = operation0.RequestBody; + Assert.Equal("object", fromFileParam0.Content.Values.Single().Schema.Type); + Assert.True(fromFileParam0.Required); + + var fromFileParam1 = operation1.RequestBody; + Assert.Equal("object", fromFileParam1.Content.Values.Single().Schema.Type); + Assert.False(fromFileParam1.Required); + } + + [Fact] + public void AddsFromFormParameterAsFormFile() + { + static void AssertFormFileParameter(OpenApiOperation operation, string expectedType, string expectedName) + { + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal(expectedType, content.Value.Schema.Type); + Assert.Equal("multipart/form-data", content.Key); + } + + AssertFormFileParameter(GetOpenApiOperation((IFormFile file) => { }), "object", "file"); + AssertFormFileParameter(GetOpenApiOperation(([FromForm(Name = "file_name")] IFormFile file) => { }), "object", "file_name"); + } + + [Fact] + public void AddsMultipartFormDataResponseFormatWhenFormFileCollectionSpecified() + { + AssertFormFileCollection((IFormFileCollection files) => Results.NoContent(), "files"); + AssertFormFileCollection(([FromForm] IFormFileCollection uploads) => Results.NoContent(), "uploads"); + + static void AssertFormFileCollection(Delegate handler, string expectedName) + { + // Arrange + var operation = GetOpenApiOperation(handler); + + // Assert + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("multipart/form-data", content.Key); + Assert.Equal("object", content.Value.Schema.Type); + Assert.True(requestBody.Required); + } + } + +#nullable restore + + [Fact] + public void HandlesEndpointWithDescriptionAndSummary_WithAttributes() + { + var operation = GetOpenApiOperation( + [EndpointSummary("A summary")][EndpointDescription("A description")] (int id) => ""); + + // Assert + Assert.Equal("A description", operation.Description); + Assert.Equal("A summary", operation.Summary); + } + + private static OpenApiOperation GetOpenApiOperation( + Delegate action, + string pattern = null, + IEnumerable httpMethods = null, + string displayName = null, + object[] additionalMetadata = null) + { + var methodInfo = action.Method; + var attributes = methodInfo.GetCustomAttributes(); + + var httpMethodMetadata = new HttpMethodMetadata(httpMethods ?? new[] { "GET" }); + var hostEnvironment = new HostEnvironment() { ApplicationName = nameof(OpenApiOperationGeneratorTests) }; + var metadataItems = new List(attributes) { methodInfo, httpMethodMetadata }; + metadataItems.AddRange(additionalMetadata ?? Array.Empty()); + var endpointMetadata = new EndpointMetadataCollection(metadataItems.ToArray()); + var routePattern = RoutePatternFactory.Parse(pattern ?? "/"); + + var generator = new OpenApiGenerator( + hostEnvironment, + new ServiceProviderIsService()); + + return generator.GetOpenApiOperation(methodInfo, endpointMetadata, routePattern); + } + + private static void TestAction() + { + } + + private class ServiceProviderIsService : IServiceProviderIsService + { + public bool IsService(Type serviceType) => serviceType == typeof(IInferredServiceInterface); + } + + private class HostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } + + private class InferredJsonClass + { + } + + private interface IInferredJsonInterface + { + } + + private record TryParseStringRecord(int Value) + { + public static bool TryParse(string value, out TryParseStringRecord result) => + throw new NotImplementedException(); + } + + private record struct TryParseStringRecordStruct(int Value) + { + public static bool TryParse(string value, out TryParseStringRecordStruct result) => + throw new NotImplementedException(); + } + + private interface IInferredServiceInterface + { + } + + private record BindAsyncRecord(int Value) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + public static bool TryParse(string value, out BindAsyncRecord result) => + throw new NotImplementedException(); + } +} diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 1c25bbede9c7..7af7e03f097d 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -507,6 +507,7 @@ private static RouteHandlerBuilder Map( defaultOrder) { DisplayName = pattern.RawText ?? pattern.DebuggerToString(), + ServiceProvider = endpoints.ServiceProvider, }; // Methods defined in a top-level program are generated as statics so the delegate diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj index bd9def6c3dc2..33d0e2038550 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj +++ b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj @@ -35,6 +35,7 @@ + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/WebApi-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/WebApi-CSharp.csproj.in index 4b3f835282a0..5b949b47fba0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/WebApi-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/WebApi-CSharp.csproj.in @@ -14,6 +14,7 @@ + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs index 2ce30ac64fd0..8af0c06784f2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs @@ -5,6 +5,9 @@ #if (WindowsAuth) using Microsoft.AspNetCore.Authentication.Negotiate; #endif +#if (EnableOpenAPI) +using Microsoft.AspNetCore.OpenApi; +#endif #if (GenerateGraph) using Graph = Microsoft.Graph; #endif From 0659fb3c6d03d42cd641257a96aa3ee37b22dc50 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 13:56:20 -0700 Subject: [PATCH 2/7] Address feedback from peer review --- AspNetCore.sln | 19 ++++ src/Http/HttpAbstractions.slnf | 1 + .../src/Microsoft.AspNetCore.OpenApi.csproj | 4 + src/Http/OpenApi/src/OpenApiGenerator.cs | 14 +-- .../OpenApiRouteHandlerBuilderExtensions.cs | 3 +- .../OpenApi/src/Properties/AssemblyInfo.cs | 6 -- .../Microsoft.AspNetCore.OpenApi.Tests.csproj | 8 +- .../OpenApi/test/OpenApiGeneratorTests.cs | 90 +++++++++---------- ...penApiRouteHandlerBuilderExtensionTests.cs | 65 ++++++++++++++ .../Routing/src/Properties/AssemblyInfo.cs | 1 + 10 files changed, 140 insertions(+), 71 deletions(-) delete mode 100644 src/Http/OpenApi/src/Properties/AssemblyInfo.cs create mode 100644 src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 4a5c1b153e6f..2a3f190371dd 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1694,6 +1694,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\Http\Http.Results\tools\ResultsOfTGenerator\ResultsOfTGenerator.csproj", "{9716D0D0-2251-44DD-8596-67D253EEF41C}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{85520C50-CF33-4A27-BEC9-272100870D9D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2D2D1107-7389-473B-BDCE-BFA060EAC453}" @@ -10147,6 +10149,22 @@ Global {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|arm64.ActiveCfg = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|arm64.Build.0 = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x64.ActiveCfg = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x64.Build.0 = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|x86.Build.0 = Debug|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|Any CPU.Build.0 = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|arm64.ActiveCfg = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|arm64.Build.0 = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.ActiveCfg = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.Build.0 = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.ActiveCfg = Release|Any CPU + {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.Build.0 = Release|Any CPU {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11018,6 +11036,7 @@ Global {B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} {489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} {8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D} + {9716D0D0-2251-44DD-8596-67D253EEF41C} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9} {2D2D1107-7389-473B-BDCE-BFA060EAC453} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} {E47D1385-64B3-429B-9B1D-B0D0B7B6E506} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} {25642C23-0BB8-4FF7-9181-9599489679EB} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index f57c31fe5b62..04c74f516abc 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -42,6 +42,7 @@ "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", + "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", diff --git a/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj index b0d92ee2ff37..1fcc11cfa048 100644 --- a/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj +++ b/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj @@ -22,4 +22,8 @@ + + + + \ No newline at end of file diff --git a/src/Http/OpenApi/src/OpenApiGenerator.cs b/src/Http/OpenApi/src/OpenApiGenerator.cs index dbb4aa6b3355..1ca19f45e435 100644 --- a/src/Http/OpenApi/src/OpenApiGenerator.cs +++ b/src/Http/OpenApi/src/OpenApiGenerator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -27,7 +28,8 @@ internal class OpenApiGenerator { private readonly IHostEnvironment? _environment; private readonly IServiceProviderIsService? _serviceProviderIsService; - private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); + + private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); /// /// Creates an instance given a @@ -190,7 +192,7 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM foreach (var annotation in eligibileAnnotations) { - var statusCode = $"{annotation.Key}"; + var statusCode = annotation.Key.ToString(CultureInfo.InvariantCulture); var (type, contentTypes) = annotation.Value; var responseContent = new Dictionary(); @@ -311,7 +313,7 @@ private static void GenerateDefaultResponses(Dictionary GetOperationTags(MethodInfo methodInfo, EndpointMetadataCollection metadata) + private List GetOperationTags(MethodInfo methodInfo, EndpointMetadataCollection metadata) { var tags = metadata.GetMetadata(); string controllerName; @@ -353,7 +355,7 @@ private IList GetOperationTags(MethodInfo methodInfo, EndpointMetada : new List() { new OpenApiTag() { Name = controllerName } }; } - private IList GetOpenApiParameters(MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern, bool disableInferredBody) + private List GetOpenApiParameters(MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern, bool disableInferredBody) { var parameters = methodInfo.GetParameters(); var openApiParameters = new List(); @@ -388,7 +390,7 @@ private IList GetOpenApiParameters(MethodInfo methodInfo, Endp return openApiParameters; } - private static IDictionary GetOpenApiParameterContent(EndpointMetadataCollection metadata) + private static Dictionary GetOpenApiParameterContent(EndpointMetadataCollection metadata) { var openApiParameterContent = new Dictionary(); var acceptsMetadata = metadata.GetMetadata(); diff --git a/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs index 3be3b3c3fe9e..63f82923d11c 100644 --- a/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs @@ -23,7 +23,6 @@ public static class OpenApiRouteHandlerBuilderExtensions /// /// The . /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder) { builder.Add(endpointBuilder => @@ -46,7 +45,7 @@ public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder) /// with the current endpoint and modifies it with the given . /// /// The . - /// An that mutates an OpenAPI annotation. + /// An that returns a new OpenAPI annotation given a generated operation. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder, Func configureOperation) { diff --git a/src/Http/OpenApi/src/Properties/AssemblyInfo.cs b/src/Http/OpenApi/src/Properties/AssemblyInfo.cs deleted file mode 100644 index d500965b263d..000000000000 --- a/src/Http/OpenApi/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj index 4641ccd2f83d..fb9105f710ea 100644 --- a/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj +++ b/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj @@ -1,8 +1,7 @@ - + $(DefaultNetCoreTargetFramework) - true @@ -17,9 +16,4 @@ - - - - - diff --git a/src/Http/OpenApi/test/OpenApiGeneratorTests.cs b/src/Http/OpenApi/test/OpenApiGeneratorTests.cs index 7a67e11b020d..459966c62f43 100644 --- a/src/Http/OpenApi/test/OpenApiGeneratorTests.cs +++ b/src/Http/OpenApi/test/OpenApiGeneratorTests.cs @@ -30,8 +30,8 @@ public void OperationNotCreatedIfNoHttpMethods() public void UsesDeclaringTypeAsOperationTags() { var operation = GetOpenApiOperation(TestAction); - var declaringTypeName = typeof(OpenApiOperationGeneratorTests).Name; + var tag = Assert.Single(operation.Tags); Assert.Equal(declaringTypeName, tag.Name); @@ -60,23 +60,17 @@ static void AssertCustomRequestFormat(OpenApiOperation operation) } AssertCustomRequestFormat(GetOpenApiOperation( - [Consumes("application/custom")] - (InferredJsonClass fromBody) => - { })); + [Consumes("application/custom")] (InferredJsonClass fromBody) => { })); AssertCustomRequestFormat(GetOpenApiOperation( - [Consumes("application/custom")] - ([FromBody] int fromBody) => - { })); + [Consumes("application/custom")] ([FromBody] int fromBody) => { })); } [Fact] public void AddsMultipleRequestFormatsFromMetadata() { var operation = GetOpenApiOperation( - [Consumes("application/custom0", "application/custom1")] - (InferredJsonClass fromBody) => - { }); + [Consumes("application/custom0", "application/custom1")] (InferredJsonClass fromBody) => { }); var request = Assert.Single(operation.Parameters); @@ -88,9 +82,7 @@ public void AddsMultipleRequestFormatsFromMetadata() public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter() { var operation = GetOpenApiOperation( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] - () => - { }); ; + [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)] () => { }); var request = operation.RequestBody; Assert.NotNull(request); @@ -107,9 +99,7 @@ public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBody public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter() { var operation = GetOpenApiOperation( - [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] - (InferredJsonClass fromBody) => - { }); + [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)] (InferredJsonClass fromBody) => { }); var request = operation.RequestBody; Assert.NotNull(request); @@ -154,7 +144,6 @@ public void AddsNoResponseFormatWhenItCannotBeInferredAndTheresNoMetadata() { static void AssertVoid(OpenApiOperation operation) { - ; var response = Assert.Single(operation.Responses); Assert.Equal("200", response.Key); Assert.Empty(response.Value.Content); @@ -170,8 +159,8 @@ public void AddsMultipleResponseFormatsFromMetadataWithPoco() { var operation = GetOpenApiOperation( [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - () => new InferredJsonClass()); + [ProducesResponseType(StatusCodes.Status400BadRequest)] + () => new InferredJsonClass()); var responses = operation.Responses; @@ -196,8 +185,8 @@ public void AddsMultipleResponseFormatsFromMetadataWithIResult() { var operation = GetOpenApiOperation( [ProducesResponseType(typeof(InferredJsonClass), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - () => Results.Ok(new InferredJsonClass())); + [ProducesResponseType(StatusCodes.Status400BadRequest)] + () => Results.Ok(new InferredJsonClass())); Assert.Equal(2, operation.Responses.Count); @@ -511,21 +500,21 @@ public void HandleMultipleProduces() // Assert Assert.Collection( - responses.OrderBy(response => response.Key), - responseType => - { - var content = Assert.Single(responseType.Value.Content); - Assert.Equal("object", content.Value.Schema.Type); - Assert.Equal("200", responseType.Key); - Assert.Equal("application/json", content.Key); - }, - responseType => - { - var content = Assert.Single(responseType.Value.Content); - Assert.Equal("object", content.Value.Schema.Type); - Assert.Equal("201", responseType.Key); - Assert.Equal("application/json", content.Key); - }); + responses.OrderBy(response => response.Key), + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("200", responseType.Key); + Assert.Equal("application/json", content.Key); + }, + responseType => + { + var content = Assert.Single(responseType.Value.Content); + Assert.Equal("object", content.Value.Schema.Type); + Assert.Equal("201", responseType.Key); + Assert.Equal("application/json", content.Key); + }); } [Fact] @@ -533,24 +522,24 @@ public void HandleAcceptsMetadata() { // Arrange var operation = GetOpenApiOperation(() => "", - additionalMetadata: new[] - { + additionalMetadata: new[] + { new AcceptsMetadata(typeof(string), true, new string[] { "application/json", "application/xml"}) - }); + }); var requestBody = operation.RequestBody; // Assert Assert.Collection( - requestBody.Content, - parameter => - { - Assert.Equal("application/json", parameter.Key); - }, - parameter => - { - Assert.Equal("application/xml", parameter.Key); - }); + requestBody.Content, + parameter => + { + Assert.Equal("application/json", parameter.Key); + }, + parameter => + { + Assert.Equal("application/xml", parameter.Key); + }); } [Fact] @@ -767,12 +756,13 @@ private static void TestAction() { } - private class ServiceProviderIsService : IServiceProviderIsService + // Shared with OpenApiRouteHandlerExtensionsTests + internal class ServiceProviderIsService : IServiceProviderIsService { public bool IsService(Type serviceType) => serviceType == typeof(IInferredServiceInterface); } - private class HostEnvironment : IHostEnvironment + internal class HostEnvironment : IHostEnvironment { public string EnvironmentName { get; set; } public string ApplicationName { get; set; } diff --git a/src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs b/src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs new file mode 100644 index 000000000000..97be66d20a46 --- /dev/null +++ b/src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using static Microsoft.AspNetCore.OpenApi.Tests.OpenApiOperationGeneratorTests; + +namespace Microsoft.AspNetCore.OpenApi.Tests; + +public class OpenApiRouteHandlerBuilderExtensionTests +{ + [Fact] + public void WithOpenApi_CanSetOperationInMetadata() + { + var hostEnvironment = new HostEnvironment() { ApplicationName = nameof(OpenApiOperationGeneratorTests) }; + var serviceProviderIsService = new ServiceProviderIsService(); + var serviceProvider = new ServiceCollection() + .AddSingleton(serviceProviderIsService) + .AddSingleton(hostEnvironment) + .BuildServiceProvider(); + + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); + string GetString() => "Foo"; + _ = builder.MapDelete("/", GetString).WithOpenApi(); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var operation = endpoint.Metadata.GetMetadata(); + Assert.NotNull(operation); + Assert.Single(operation.Responses); // Sanity check generated operation + } + + [Fact] + public void WithOpenApi_CanSetOperationInMetadataWithOverride() + { + var hostEnvironment = new HostEnvironment() { ApplicationName = nameof(OpenApiOperationGeneratorTests) }; + var serviceProviderIsService = new ServiceProviderIsService(); + var serviceProvider = new ServiceCollection() + .AddSingleton(serviceProviderIsService) + .AddSingleton(hostEnvironment) + .BuildServiceProvider(); + + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); + string GetString() => "Foo"; + _ = builder.MapDelete("/", GetString).WithOpenApi(generatedOperation => new OpenApiOperation()); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var operation = endpoint.Metadata.GetMetadata(); + Assert.NotNull(operation); + Assert.Empty(operation.Responses); + } + + private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); + } +} diff --git a/src/Http/Routing/src/Properties/AssemblyInfo.cs b/src/Http/Routing/src/Properties/AssemblyInfo.cs index 93c67c32e9c9..f9e75169e796 100644 --- a/src/Http/Routing/src/Properties/AssemblyInfo.cs +++ b/src/Http/Routing/src/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Microbenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ApiExplorer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] From ecd00c5b2cae4efb3d247d734e518b0a693d7ad1 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 15:08:58 -0700 Subject: [PATCH 3/7] Move OpenApi package to top-level directory in src --- AspNetCore.sln | 4 ++-- eng/ProjectReferences.props | 2 +- src/OpenApi/OpenApi.slnf | 14 ++++++++++++++ src/OpenApi/README.md | 5 +++++ src/OpenApi/build.cmd | 3 +++ src/OpenApi/build.sh | 7 +++++++ .../src/Microsoft.AspNetCore.OpenApi.csproj | 0 src/{Http => }/OpenApi/src/OpenApiGenerator.cs | 0 .../src/OpenApiRouteHandlerBuilderExtensions.cs | 0 src/{Http => }/OpenApi/src/PublicAPI.Shipped.txt | 0 src/{Http => }/OpenApi/src/PublicAPI.Unshipped.txt | 0 src/{Http => }/OpenApi/src/SchemaGenerator.cs | 0 src/OpenApi/startvs.cmd | 3 +++ .../test/Microsoft.AspNetCore.OpenApi.Tests.csproj | 0 .../OpenApi/test/OpenApiGeneratorTests.cs | 0 .../OpenApiRouteHandlerBuilderExtensionTests.cs | 0 .../Microsoft.DotNet.Web.ProjectTemplates.csproj | 2 +- 17 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/OpenApi/OpenApi.slnf create mode 100644 src/OpenApi/README.md create mode 100644 src/OpenApi/build.cmd create mode 100644 src/OpenApi/build.sh rename src/{Http => }/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj (100%) rename src/{Http => }/OpenApi/src/OpenApiGenerator.cs (100%) rename src/{Http => }/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs (100%) rename src/{Http => }/OpenApi/src/PublicAPI.Shipped.txt (100%) rename src/{Http => }/OpenApi/src/PublicAPI.Unshipped.txt (100%) rename src/{Http => }/OpenApi/src/SchemaGenerator.cs (100%) create mode 100644 src/OpenApi/startvs.cmd rename src/{Http => }/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj (100%) rename src/{Http => }/OpenApi/test/OpenApiGeneratorTests.cs (100%) rename src/{Http => }/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs (100%) diff --git a/AspNetCore.sln b/AspNetCore.sln index 2a3f190371dd..7b12f823dc5e 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1704,9 +1704,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E47D1385-64B EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{25642C23-0BB8-4FF7-9181-9599489679EB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi", "src\Http\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi", "src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\Http\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{77305727-1A53-402A-A4E8-4CFA0DBFACC6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{77305727-1A53-402A-A4E8-4CFA0DBFACC6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 0411bbd7c587..0e2f3d409bd2 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -31,7 +31,7 @@ - + diff --git a/src/OpenApi/OpenApi.slnf b/src/OpenApi/OpenApi.slnf new file mode 100644 index 000000000000..0311c6b7ddcd --- /dev/null +++ b/src/OpenApi/OpenApi.slnf @@ -0,0 +1,14 @@ +{ + "solution": { + "path": "..\\..\\AspNetCore.sln", + "projects": [ + "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", + "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", + "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", + "src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj", + "src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj" + ] + } +} \ No newline at end of file diff --git a/src/OpenApi/README.md b/src/OpenApi/README.md new file mode 100644 index 000000000000..345489e4105b --- /dev/null +++ b/src/OpenApi/README.md @@ -0,0 +1,5 @@ +# Microsoft.AspNetCore.OpenApi + +This directory contains the source for the `Microsoft.AspNetCore.OpenApi` package which provides support for +generating OpenApi schemas directly for route handler endpoints in ASP.NET Core. + diff --git a/src/OpenApi/build.cmd b/src/OpenApi/build.cmd new file mode 100644 index 000000000000..cee462c96fa1 --- /dev/null +++ b/src/OpenApi/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\eng\build.cmd -projects %~dp0**\*.*proj %* \ No newline at end of file diff --git a/src/OpenApi/build.sh b/src/OpenApi/build.sh new file mode 100644 index 000000000000..491835c0cb2b --- /dev/null +++ b/src/OpenApi/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +repo_root="$DIR/../.." +"$repo_root/eng/build.sh" --projects "$DIR/**/*.*proj" "$@" \ No newline at end of file diff --git a/src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj similarity index 100% rename from src/Http/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj rename to src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj diff --git a/src/Http/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs similarity index 100% rename from src/Http/OpenApi/src/OpenApiGenerator.cs rename to src/OpenApi/src/OpenApiGenerator.cs diff --git a/src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs b/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs similarity index 100% rename from src/Http/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs rename to src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs diff --git a/src/Http/OpenApi/src/PublicAPI.Shipped.txt b/src/OpenApi/src/PublicAPI.Shipped.txt similarity index 100% rename from src/Http/OpenApi/src/PublicAPI.Shipped.txt rename to src/OpenApi/src/PublicAPI.Shipped.txt diff --git a/src/Http/OpenApi/src/PublicAPI.Unshipped.txt b/src/OpenApi/src/PublicAPI.Unshipped.txt similarity index 100% rename from src/Http/OpenApi/src/PublicAPI.Unshipped.txt rename to src/OpenApi/src/PublicAPI.Unshipped.txt diff --git a/src/Http/OpenApi/src/SchemaGenerator.cs b/src/OpenApi/src/SchemaGenerator.cs similarity index 100% rename from src/Http/OpenApi/src/SchemaGenerator.cs rename to src/OpenApi/src/SchemaGenerator.cs diff --git a/src/OpenApi/startvs.cmd b/src/OpenApi/startvs.cmd new file mode 100644 index 000000000000..f568dedcccee --- /dev/null +++ b/src/OpenApi/startvs.cmd @@ -0,0 +1,3 @@ +@ECHO OFF + +%~dp0..\..\startvs.cmd %~dp0OpenApi.slnf \ No newline at end of file diff --git a/src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj similarity index 100% rename from src/Http/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj diff --git a/src/Http/OpenApi/test/OpenApiGeneratorTests.cs b/src/OpenApi/test/OpenApiGeneratorTests.cs similarity index 100% rename from src/Http/OpenApi/test/OpenApiGeneratorTests.cs rename to src/OpenApi/test/OpenApiGeneratorTests.cs diff --git a/src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs b/src/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs similarity index 100% rename from src/Http/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs rename to src/OpenApi/test/OpenApiRouteHandlerBuilderExtensionTests.cs diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj index 33d0e2038550..2dc413643421 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj +++ b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj @@ -35,12 +35,12 @@ - + From 147083021581d7601b7528ac0d4c843e0af5cb1f Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 15:29:50 -0700 Subject: [PATCH 4/7] Clean up HttpAbstractions filter --- src/Http/HttpAbstractions.slnf | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 04c74f516abc..13aff7c58979 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -26,8 +26,6 @@ "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Http\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj", - "src\\Http\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj", "src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj", "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", @@ -45,14 +43,8 @@ "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", - "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", - "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", - "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj", "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", - "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", - "src\\Mvc\\Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj", - "src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", @@ -60,10 +52,9 @@ "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", - "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj", "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} From 5b8ff64adcba2427dc39981977cf677dff518498 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 16:19:15 -0700 Subject: [PATCH 5/7] Update ProjectReferences and templates --- src/Http/HttpAbstractions.slnf | 2 +- .../Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs | 4 ++++ .../WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 13aff7c58979..70fc8b56f6a8 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -57,4 +57,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs index 2329d42dafe3..b6ec2e136ef7 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs @@ -3,6 +3,9 @@ #endif using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; +#if (EnableOpenAPI) +using Microsoft.AspNetCore.OpenApi; +#endif #if (GenerateGraph) using Graph = Microsoft.Graph; #endif @@ -134,6 +137,7 @@ #if (EnableOpenAPI) }) .WithName("GetWeatherForecast") +.WithOpenApi() .RequireAuthorization(); #else }) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs index 8b8e2b81afd0..1ef820c6fb6a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs @@ -1,3 +1,7 @@ +#if (EnableOpenAPI) +using Microsoft.AspNetCore.OpenApi; + +#endif #if (WindowsAuth) using Microsoft.AspNetCore.Authentication.Negotiate; @@ -57,7 +61,8 @@ return forecast; #if (EnableOpenAPI) }) -.WithName("GetWeatherForecast"); +.WithName("GetWeatherForecast") +.WithOpenApi(); #else }); #endif From 1f017793ece760fd5a7739b8041b7dca53fa616f Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 17:19:03 -0700 Subject: [PATCH 6/7] Fix up ProjectReferences and address feedback --- eng/ProjectReferences.props | 2 +- src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj | 1 - src/OpenApi/src/OpenApiGenerator.cs | 10 +++++----- .../src/OpenApiRouteHandlerBuilderExtensions.cs | 2 -- .../test/Microsoft.AspNetCore.OpenApi.Tests.csproj | 4 ++-- .../content/WebApi-CSharp/Program.cs | 3 --- 6 files changed, 8 insertions(+), 14 deletions(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 0e2f3d409bd2..33d4d05ed215 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -31,7 +31,6 @@ - @@ -160,5 +159,6 @@ + diff --git a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj index 1fcc11cfa048..91cc8230727b 100644 --- a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj +++ b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj @@ -5,7 +5,6 @@ true aspnetcore;openapi Provides APIs for annotating route handler endpoints in ASP.NET Core with OpenAPI annotations. - enable diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index 1ca19f45e435..e8f2f59d5b11 100644 --- a/src/OpenApi/src/OpenApiGenerator.cs +++ b/src/OpenApi/src/OpenApiGenerator.cs @@ -29,14 +29,14 @@ internal class OpenApiGenerator private readonly IHostEnvironment? _environment; private readonly IServiceProviderIsService? _serviceProviderIsService; - private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); + internal static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); /// - /// Creates an instance given a - /// and a instance. + /// Creates an instance given an + /// and an instance. /// /// The host environment. - /// The service to determine if the a type is available from the . + /// The service to determine if the type is available from the . internal OpenApiGenerator( IHostEnvironment? environment, IServiceProviderIsService? serviceProviderIsService) @@ -405,7 +405,7 @@ private static Dictionary GetOpenApiParameterContent(E return openApiParameterContent; } - private (bool, ParameterLocation?) GetOpenApiParameterLocation(ParameterInfo parameter, RoutePattern pattern, bool disableInferredBody) + private (bool isBodyOrForm, ParameterLocation? locatedIn) GetOpenApiParameterLocation(ParameterInfo parameter, RoutePattern pattern, bool disableInferredBody) { var attributes = parameter.GetCustomAttributes(); diff --git a/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs b/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs index 63f82923d11c..878ecc2bdfe0 100644 --- a/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs @@ -37,7 +37,6 @@ public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder) }; }); return builder; - } /// @@ -58,7 +57,6 @@ public static RouteHandlerBuilder WithOpenApi(this RouteHandlerBuilder builder, { routeEndpointBuilder.Metadata.Add(configureOperation(openApiOperation)); } - }; }); return builder; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj index fb9105f710ea..efa9290e2325 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -10,10 +10,10 @@ - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs index 8af0c06784f2..2ce30ac64fd0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs @@ -5,9 +5,6 @@ #if (WindowsAuth) using Microsoft.AspNetCore.Authentication.Negotiate; #endif -#if (EnableOpenAPI) -using Microsoft.AspNetCore.OpenApi; -#endif #if (GenerateGraph) using Graph = Microsoft.Graph; #endif From a158b787d333a88441edcbe395b15141a0cb97e9 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Apr 2022 18:18:30 -0700 Subject: [PATCH 7/7] Add new directory to Build.props and clean up sln --- AspNetCore.sln | 84 ++++++++++++++++++++++--------------------------- eng/Build.props | 2 ++ 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/AspNetCore.sln b/AspNetCore.sln index 7b12f823dc5e..0ba416a9b244 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1696,17 +1696,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\Http\Http.Results\tools\ResultsOfTGenerator\ResultsOfTGenerator.csproj", "{9716D0D0-2251-44DD-8596-67D253EEF41C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{85520C50-CF33-4A27-BEC9-272100870D9D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2D2D1107-7389-473B-BDCE-BFA060EAC453}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{3AEFB466-6310-4F3F-923F-9154224E3629}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E47D1385-64B3-429B-9B1D-B0D0B7B6E506}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{25642C23-0BB8-4FF7-9181-9599489679EB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi", "src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{77305727-1A53-402A-A4E8-4CFA0DBFACC6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi", "src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -10165,38 +10159,38 @@ Global {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.Build.0 = Release|Any CPU {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.ActiveCfg = Release|Any CPU {9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.Build.0 = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|arm64.ActiveCfg = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|arm64.Build.0 = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x64.ActiveCfg = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x64.Build.0 = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x86.ActiveCfg = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Debug|x86.Build.0 = Debug|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|Any CPU.Build.0 = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|arm64.ActiveCfg = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|arm64.Build.0 = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x64.ActiveCfg = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x64.Build.0 = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x86.ActiveCfg = Release|Any CPU - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4}.Release|x86.Build.0 = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|arm64.ActiveCfg = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|arm64.Build.0 = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x64.ActiveCfg = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x64.Build.0 = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x86.ActiveCfg = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Debug|x86.Build.0 = Debug|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|Any CPU.Build.0 = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|arm64.ActiveCfg = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|arm64.Build.0 = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x64.ActiveCfg = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x64.Build.0 = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x86.ActiveCfg = Release|Any CPU - {77305727-1A53-402A-A4E8-4CFA0DBFACC6}.Release|x86.Build.0 = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|arm64.ActiveCfg = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|arm64.Build.0 = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|x64.ActiveCfg = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|x64.Build.0 = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|x86.ActiveCfg = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Debug|x86.Build.0 = Debug|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|Any CPU.Build.0 = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|arm64.ActiveCfg = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|arm64.Build.0 = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|x64.ActiveCfg = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|x64.Build.0 = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|x86.ActiveCfg = Release|Any CPU + {3AEFB466-6310-4F3F-923F-9154224E3629}.Release|x86.Build.0 = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|arm64.ActiveCfg = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|arm64.Build.0 = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|x64.Build.0 = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Debug|x86.Build.0 = Debug|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|Any CPU.Build.0 = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|arm64.ActiveCfg = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|arm64.Build.0 = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x64.ActiveCfg = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x64.Build.0 = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x86.ActiveCfg = Release|Any CPU + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11037,11 +11031,9 @@ Global {489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} {8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D} {9716D0D0-2251-44DD-8596-67D253EEF41C} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9} - {2D2D1107-7389-473B-BDCE-BFA060EAC453} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} - {E47D1385-64B3-429B-9B1D-B0D0B7B6E506} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} - {25642C23-0BB8-4FF7-9181-9599489679EB} = {2D2D1107-7389-473B-BDCE-BFA060EAC453} - {EF5062E5-93FA-48B3-B8DB-7B7B263A64D4} = {E47D1385-64B3-429B-9B1D-B0D0B7B6E506} - {77305727-1A53-402A-A4E8-4CFA0DBFACC6} = {25642C23-0BB8-4FF7-9181-9599489679EB} + {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} + {3AEFB466-6310-4F3F-923F-9154224E3629} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} + {EFC8EA45-572D-4D8D-A597-9045A2D8EC40} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/Build.props b/eng/Build.props index a39342be7da3..afd7af63effd 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -198,6 +198,7 @@ $(RepoRoot)src\submodules\spa-templates\src\*.csproj; $(RepoRoot)src\Extensions\**\*.csproj; $(RepoRoot)src\BuildAfterTargetingPack\*.csproj; + $(RepoRoot)src\OpenApi\**\*.csproj; " Exclude=" @(ProjectToBuild); @@ -238,6 +239,7 @@ $(RepoRoot)src\Testing\**\src\*.csproj; $(RepoRoot)src\Extensions\**\src\*.csproj; $(RepoRoot)src\BuildAfterTargetingPack\*.csproj; + $(RepoRoot)src\OpenApi\**\src\*.csproj; " Exclude=" @(ProjectToBuild);