Skip to content

Commit d28d0c5

Browse files
Enable trimming for OpenAPI package (#55465)
Co-authored-by: Eric Erhardt <[email protected]>
1 parent 3fdb60a commit d28d0c5

17 files changed

+133
-24
lines changed

eng/TrimmableProjects.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,6 @@
107107
<TrimmableProject Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
108108
<TrimmableProject Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
109109
<TrimmableProject Include="Microsoft.Extensions.Features" />
110+
<TrimmableProject Include="Microsoft.AspNetCore.OpenApi" />
110111
</ItemGroup>
111112
</Project>

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I
107107
{
108108
AssigningCode = "httpContext.Request.Form";
109109
}
110+
// Complex form binding is only supported in RDF because it uses shared source with Blazor that requires dynamic analysis
111+
// and codegen. Emit a diagnostic when these are encountered to avoid producing buggy code.
112+
else if (!(SymbolEqualityComparer.Default.Equals(Type, wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_Primitives_StringValues))
113+
|| Type.SpecialType == SpecialType.System_String
114+
|| TryGetParsability(Type, wellKnownTypes, out var _)
115+
|| (IsArray && TryGetParsability(ElementType, wellKnownTypes, out var _))))
116+
{
117+
var location = endpoint.Operation.Syntax.GetLocation();
118+
endpoint.Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnableToResolveParameterDescriptor, location, symbol.Name));
119+
}
110120
else
111121
{
112122
AssigningCode = !IsArray

src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,24 @@ public class ConcreteService : IGenericService<SomeInput, string?>
737737
var diagnostics = updatedCompilation.GetDiagnostics();
738738
Assert.Empty(diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning));
739739
}
740+
741+
[Fact]
742+
public async Task RequestDelegateGenerator_SkipsComplexFormParameter()
743+
{
744+
var source = """
745+
app.MapPost("/", ([FromForm] Todo todo) => { });
746+
app.MapPost("/", ([FromForm] Todo todo, IFormFile formFile) => { });
747+
app.MapPost("/", ([FromForm] Todo todo, [FromForm] int[] ids) => { });
748+
""";
749+
var (generatorRunResult, _) = await RunGeneratorAsync(source);
750+
751+
// Emits diagnostics but no generated sources
752+
var result = Assert.IsType<GeneratorRunResult>(generatorRunResult);
753+
Assert.Empty(result.GeneratedSources);
754+
Assert.All(result.Diagnostics, diagnostic =>
755+
{
756+
Assert.Equal(DiagnosticDescriptors.UnableToResolveParameterDescriptor.Id, diagnostic.Id);
757+
Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
758+
});
759+
}
740760
}

src/OpenApi/sample/Controllers/TestController.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using System.Diagnostics.CodeAnalysis;
23
using Microsoft.AspNetCore.Mvc;
34

45
[ApiController]
@@ -26,6 +27,7 @@ public class RouteParamsContainer
2627

2728
[FromRoute]
2829
[MinLength(5)]
30+
[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", Justification = "MinLengthAttribute works without reflection on string properties.")]
2931
public string? Name { get; set; }
3032
}
3133

src/OpenApi/sample/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
var builder = WebApplication.CreateBuilder(args);
99

10+
#pragma warning disable IL2026 // MVC isn't trim-friendly yet
1011
builder.Services.AddControllers();
12+
#pragma warning restore IL2026
1113
builder.Services.AddAuthentication().AddJwtBearer();
1214

1315
builder.Services.AddOpenApi("v1", options =>
@@ -44,8 +46,15 @@
4446
forms.MapPost("/form-file", (IFormFile resume) => Results.Ok(resume.FileName));
4547
forms.MapPost("/form-files", (IFormFileCollection files) => Results.Ok(files.Count));
4648
forms.MapPost("/form-file-multiple", (IFormFile resume, IFormFileCollection files) => Results.Ok(files.Count + resume.FileName));
49+
// Disable warnings because RDG does not support complex form binding yet.
50+
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
51+
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
52+
#pragma warning disable RDG003 // Unable to resolve parameter
4753
forms.MapPost("/form-todo", ([FromForm] Todo todo) => Results.Ok(todo));
4854
forms.MapPost("/forms-pocos-and-files", ([FromForm] Todo todo, IFormFile file) => Results.Ok(new { Todo = todo, File = file.FileName }));
55+
#pragma warning restore RDG003 // Unable to resolve parameter
56+
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
57+
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
4958

5059
var v1 = app.MapGroup("v1")
5160
.WithGroupName("v1");

src/OpenApi/sample/Sample.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7+
<!-- Required to generated trimmable Map-invocations. -->
8+
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
9+
<IsAotCompatible>true</IsAotCompatible>
710
</PropertyGroup>
811

912
<ItemGroup>
@@ -22,4 +25,9 @@
2225
<Compile Include="../test/SharedTypes.cs" />
2326
</ItemGroup>
2427

28+
<!-- Required to generated trimmable Map-invocations. -->
29+
<ItemGroup>
30+
<ProjectReference Include="$(RepoRoot)/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
31+
</ItemGroup>
32+
2533
</Project>

src/OpenApi/src/Extensions/JsonObjectSchemaExtensions.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.ComponentModel.DataAnnotations;
55
using System.Globalization;
66
using System.Linq;
7-
using System.Reflection;
87
using System.Text.Json.Nodes;
98
using JsonSchemaMapper;
109
using Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -248,14 +247,10 @@ internal static void ApplyParameterInfo(this JsonObject schema, ApiParameterDesc
248247
// based on our model binding heuristics. In that case, to access the validation attributes that the
249248
// model binder will respect we will need to get the property from the container type and map the
250249
// attributes on it to the schema.
251-
if (parameterDescription.ModelMetadata.PropertyName is { } propertyName)
250+
if (parameterDescription.ModelMetadata is { PropertyName: { }, ContainerType: { }, HasValidators: true, ValidatorMetadata: { } validations })
252251
{
253-
var property = parameterDescription.ModelMetadata.ContainerType?.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
254-
if (property is not null)
255-
{
256-
var attributes = property.GetCustomAttributes(true).OfType<ValidationAttribute>();
257-
schema.ApplyValidationAttributes(attributes);
258-
}
252+
var attributes = validations.OfType<ValidationAttribute>();
253+
schema.ApplyValidationAttributes(attributes);
259254
}
260255
// Route constraints are only defined on parameters that are sourced from the path. Since
261256
// they are encoded in the route template, and not in the type information based to the underlying

src/OpenApi/src/Extensions/OpenApiEndpointConventionBuilderExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using System.Reflection;
67
using Microsoft.AspNetCore.Http;
@@ -17,12 +18,16 @@ namespace Microsoft.AspNetCore.Builder;
1718
/// </summary>
1819
public static class OpenApiEndpointConventionBuilderExtensions
1920
{
21+
private const string TrimWarningMessage = "Calls Microsoft.AspNetCore.OpenApi.OpenApiGenerator.GetOpenApiOperation(MethodInfo, EndpointMetadataCollection, RoutePattern) which uses dynamic analysis. Use IServiceCollection.AddOpenApi() to generate OpenAPI metadata at startup for all endpoints,";
22+
2023
/// <summary>
2124
/// Adds an OpenAPI annotation to <see cref="Endpoint.Metadata" /> associated
2225
/// with the current endpoint.
2326
/// </summary>
2427
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
2528
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
29+
[RequiresDynamicCode(TrimWarningMessage)]
30+
[RequiresUnreferencedCode(TrimWarningMessage)]
2631
public static TBuilder WithOpenApi<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
2732
{
2833
builder.Finally(builder => AddAndConfigureOperationForEndpoint(builder));
@@ -36,13 +41,17 @@ public static TBuilder WithOpenApi<TBuilder>(this TBuilder builder) where TBuild
3641
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
3742
/// <param name="configureOperation">An <see cref="Func{T, TResult}"/> that returns a new OpenAPI annotation given a generated operation.</param>
3843
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
44+
[RequiresDynamicCode(TrimWarningMessage)]
45+
[RequiresUnreferencedCode(TrimWarningMessage)]
3946
public static TBuilder WithOpenApi<TBuilder>(this TBuilder builder, Func<OpenApiOperation, OpenApiOperation> configureOperation)
4047
where TBuilder : IEndpointConventionBuilder
4148
{
4249
builder.Finally(endpointBuilder => AddAndConfigureOperationForEndpoint(endpointBuilder, configureOperation));
4350
return builder;
4451
}
4552

53+
[RequiresDynamicCode(TrimWarningMessage)]
54+
[RequiresUnreferencedCode(TrimWarningMessage)]
4655
private static void AddAndConfigureOperationForEndpoint(EndpointBuilder endpointBuilder, Func<OpenApiOperation, OpenApiOperation>? configure = null)
4756
{
4857
foreach (var item in endpointBuilder.Metadata)

src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Http.Json;
45
using Microsoft.AspNetCore.OpenApi;
56
using Microsoft.Extensions.ApiDescriptions;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Extensions.Options;
69

710
namespace Microsoft.Extensions.DependencyInjection;
811

@@ -67,6 +70,8 @@ private static IServiceCollection AddOpenApiCore(this IServiceCollection service
6770
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
6871
// Required to resolve document names for build-time generation
6972
services.AddSingleton(new NamedService<OpenApiDocumentService>(documentName));
73+
// Required to support JSON serializations
74+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, OpenApiSchemaJsonOptions>());
7075
return services;
7176
}
7277
}

src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<!-- Needed to support compiling Utf8BufferTextWriter implementation shared with SignalR -->
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
99
<Description>Provides APIs for annotating route handler endpoints in ASP.NET Core with OpenAPI annotations.</Description>
10+
<IsAotCompatible>true</IsAotCompatible>
11+
<!-- Required to generated native AoT friendly `MapOpenApi` endpoint. -->
12+
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
13+
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsPreviewNamespaces>
1014
</PropertyGroup>
1115

1216
<ItemGroup>
@@ -36,4 +40,9 @@
3640
<Compile Include="$(RepoRoot)/src/SignalR/common/Shared/MemoryBufferWriter.cs" LinkBase="Shared" />
3741
</ItemGroup>
3842

43+
<!-- Required to generated native AoT friendly `MapOpenApi` endpoint. -->
44+
<ItemGroup>
45+
<ProjectReference Include="$(RepoRoot)/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
46+
</ItemGroup>
47+
3948
</Project>

0 commit comments

Comments
 (0)