diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index ffde1bd5b9bb..de2fe7826834 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -25,6 +25,7 @@ + diff --git a/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs index 029768ef6662..f155b7701f81 100644 --- a/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs +++ b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs @@ -358,6 +358,57 @@ public void CreateFromObject_MixedCaseThrows() Assert.Equal(message, exception.Message, ignoreCase: true); } + [Fact] + public void CreateFromObject_Struct_ReadValues() + { + // Arrange + var obj = new StructAddress() { City = "Singapore" }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("City", kvp.Key); Assert.Equal("Singapore", kvp.Value); }, + kvp => { Assert.Equal("State", kvp.Key); Assert.Null(kvp.Value); }); + } + + [Fact] + public void CreateFromObject_NullableStruct_ReadValues() + { + // Arrange + StructAddress? obj = new StructAddress() { City = "Singapore" }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("City", kvp.Key); Assert.Equal("Singapore", kvp.Value); }, + kvp => { Assert.Equal("State", kvp.Key); Assert.Null(kvp.Value); }); + } + + [Fact] + public void CreateFromObject_NullStruct_ReadValues() + { + // Arrange + StructAddress? obj = null; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.Null(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Empty(dict); + } + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. [Fact] public void Comparer_IsOrdinalIgnoreCase() @@ -2164,4 +2215,11 @@ private class Address public string? State { get; set; } } + + private struct StructAddress + { + public string? City { get; set; } + + public string? State { get; set; } + } } diff --git a/src/Http/Http.Results/src/AcceptedAtRoute.cs b/src/Http/Http.Results/src/AcceptedAtRoute.cs index 55489db2d29c..c3f14deb4206 100644 --- a/src/Http/Http.Results/src/AcceptedAtRoute.cs +++ b/src/Http/Http.Results/src/AcceptedAtRoute.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -22,11 +24,24 @@ public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider, IStatu /// provided. /// /// The route data to use for generating the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal AcceptedAtRoute(object? routeValues) : this(routeName: null, routeValues: routeValues) { } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] + internal AcceptedAtRoute(string? routeName, object? routeValues) + : this(routeName, new RouteValueDictionary(routeValues)) + { + } + /// /// Initializes a new instance of the class with the values /// provided. @@ -35,10 +50,10 @@ internal AcceptedAtRoute(object? routeValues) /// The route data to use for generating the URL. internal AcceptedAtRoute( string? routeName, - object? routeValues) + RouteValueDictionary routeValues) { RouteName = routeName; - RouteValues = new RouteValueDictionary(routeValues); + RouteValues = routeValues; } /// diff --git a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index c270926e5223..c9d85f1e9c7d 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,11 +26,25 @@ public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider /// /// The route data to use for generating the URL. /// The value to format in the entity body. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal AcceptedAtRoute(object? routeValues, TValue? value) : this(routeName: null, routeValues: routeValues, value: value) { } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] + internal AcceptedAtRoute(string? routeName, object? routeValues, TValue? value) + : this(routeName, new RouteValueDictionary(routeValues), value) + { + } + /// /// Initializes a new instance of the class with the values /// provided. @@ -38,12 +54,12 @@ internal AcceptedAtRoute(object? routeValues, TValue? value) /// The value to format in the entity body. internal AcceptedAtRoute( string? routeName, - object? routeValues, + RouteValueDictionary routeValues, TValue? value) { Value = value; RouteName = routeName; - RouteValues = new RouteValueDictionary(routeValues); + RouteValues = routeValues; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); } diff --git a/src/Http/Http.Results/src/CreatedAtRoute.cs b/src/Http/Http.Results/src/CreatedAtRoute.cs index 213576e24e52..9948e811de16 100644 --- a/src/Http/Http.Results/src/CreatedAtRoute.cs +++ b/src/Http/Http.Results/src/CreatedAtRoute.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -22,11 +24,24 @@ public sealed class CreatedAtRoute : IResult, IEndpointMetadataProvider, IStatus /// provided. /// /// The route data to use for generating the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal CreatedAtRoute(object? routeValues) : this(routeName: null, routeValues: routeValues) { } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] + internal CreatedAtRoute(string? routeName, object? routeValues) + : this(routeName, new RouteValueDictionary(routeValues)) + { + } + /// /// Initializes a new instance of the class with the values /// provided. @@ -35,10 +50,10 @@ internal CreatedAtRoute(object? routeValues) /// The route data to use for generating the URL. internal CreatedAtRoute( string? routeName, - object? routeValues) + RouteValueDictionary routeValues) { RouteName = routeName; - RouteValues = new RouteValueDictionary(routeValues); + RouteValues = routeValues; } /// diff --git a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs index 6d543d9b98f5..f5b026979c5f 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,11 +26,25 @@ public sealed class CreatedAtRoute : IResult, IEndpointMetadataProvider, /// /// The route data to use for generating the URL. /// The value to format in the entity body. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal CreatedAtRoute(object? routeValues, TValue? value) : this(routeName: null, routeValues: routeValues, value: value) { } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] + internal CreatedAtRoute(string? routeName, object? routeValues, TValue? value) + : this(routeName, new RouteValueDictionary(routeValues), value) + { + } + /// /// Initializes a new instance of the class with the values /// provided. @@ -38,12 +54,12 @@ internal CreatedAtRoute(object? routeValues, TValue? value) /// The value to format in the entity body. internal CreatedAtRoute( string? routeName, - object? routeValues, + RouteValueDictionary routeValues, TValue? value) { Value = value; RouteName = routeName; - RouteValues = new RouteValueDictionary(routeValues); + RouteValues = routeValues; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); } diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index cb7e1edf20a5..7aade3bb8ad0 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.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.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Internal; @@ -15,6 +16,9 @@ internal static partial class HttpResultsHelper internal const string DefaultContentType = "text/plain; charset=utf-8"; private static readonly Encoding DefaultEncoding = Encoding.UTF8; + // Remove once https://github.com/dotnet/aspnetcore/pull/46008 is done. + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public static Task WriteResultAsJsonAsync( HttpContext httpContext, ILogger logger, diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index 7808c52b5ec9..3b5c22a26f99 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -7,7 +7,7 @@ true aspnetcore false - enable + true Microsoft.AspNetCore.Http.Result @@ -19,6 +19,7 @@ + diff --git a/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs index 0daa608bc58d..bd823fc9c13b 100644 --- a/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs +++ b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -19,6 +21,7 @@ public sealed partial class RedirectToRouteHttpResult : IResult /// provided. /// /// The parameters for the route. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal RedirectToRouteHttpResult(object? routeValues) : this(routeName: null, routeValues: routeValues) { @@ -30,6 +33,7 @@ internal RedirectToRouteHttpResult(object? routeValues) /// /// The name of the route. /// The parameters for the route. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal RedirectToRouteHttpResult( string? routeName, object? routeValues) @@ -45,6 +49,7 @@ internal RedirectToRouteHttpResult( /// The parameters for the route. /// If set to true, makes the redirect permanent (301). /// Otherwise a temporary redirect is used (302). + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal RedirectToRouteHttpResult( string? routeName, object? routeValues, @@ -62,6 +67,7 @@ internal RedirectToRouteHttpResult( /// If set to true, makes the redirect permanent (301). /// Otherwise a temporary redirect is used (302). /// The fragment to add to the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal RedirectToRouteHttpResult( string? routeName, object? routeValues, @@ -82,15 +88,41 @@ internal RedirectToRouteHttpResult( /// If set to true, make the temporary redirect (307) /// or permanent redirect (308) preserve the initial request method. /// The fragment to add to the URL. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] internal RedirectToRouteHttpResult( string? routeName, object? routeValues, bool permanent, bool preserveMethod, + string? fragment) : this( + routeName, + routeValues == null ? null : new RouteValueDictionary(routeValues), + permanent, + preserveMethod, + fragment) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). + /// Otherwise a temporary redirect is used (302). + /// If set to true, make the temporary redirect (307) + /// or permanent redirect (308) preserve the initial request method. + /// The fragment to add to the URL. + internal RedirectToRouteHttpResult( + string? routeName, + RouteValueDictionary? routeValues, + bool permanent, + bool preserveMethod, string? fragment) { RouteName = routeName; - RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); + RouteValues = routeValues; PreserveMethod = preserveMethod; Permanent = permanent; Fragment = fragment; diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs index 40c283d7ec1b..6acc40b9e096 100644 --- a/src/Http/Http.Results/src/Results.cs +++ b/src/Http/Http.Results/src/Results.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -493,6 +494,7 @@ public static IResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, Uri /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. /// The fragment to add to the URL. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static IResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) => TypedResults.RedirectToRoute(routeName, routeValues, permanent, preserveMethod, fragment); @@ -667,6 +669,13 @@ public static IResult ValidationProblem( problemDetails.Title = title ?? problemDetails.Title; + CopyExtensions(extensions, problemDetails); + + return TypedResults.Problem(problemDetails); + } + + private static void CopyExtensions(IDictionary? extensions, HttpValidationProblemDetails problemDetails) + { if (extensions is not null) { foreach (var extension in extensions) @@ -674,8 +683,6 @@ public static IResult ValidationProblem( problemDetails.Extensions.Add(extension); } } - - return TypedResults.Problem(problemDetails); } /// @@ -728,6 +735,7 @@ public static IResult Created(Uri? uri, TValue? value) /// The route data to use for generating the URL. /// The value to be included in the HTTP response body. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static IResult CreatedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) => CreatedAtRoute(routeName, routeValues, value); @@ -738,6 +746,7 @@ public static IResult CreatedAtRoute(string? routeName = null, object? routeValu /// The route data to use for generating the URL. /// The value to be included in the HTTP response body. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult CreatedAtRoute(string? routeName = null, object? routeValues = null, TValue? value = default) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters @@ -770,6 +779,7 @@ public static IResult Accepted(string? uri = null, TValue? value = defau /// The route data to use for generating the URL. /// The optional content value to format in the response body. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] #pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. public static IResult AcceptedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) #pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. @@ -782,6 +792,7 @@ public static IResult AcceptedAtRoute(string? routeName = null, object? routeVal /// The route data to use for generating the URL. /// The optional content value to format in the response body. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult AcceptedAtRoute(string? routeName = null, object? routeValues = null, TValue? value = default) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/src/Http/Http.Results/src/ResultsOfT.Generated.cs b/src/Http/Http.Results/src/ResultsOfT.Generated.cs index 6034e656b573..3e4796d663ea 100644 --- a/src/Http/Http.Results/src/ResultsOfT.Generated.cs +++ b/src/Http/Http.Results/src/ResultsOfT.Generated.cs @@ -3,6 +3,7 @@ // This file is generated by a tool. See: src/Http/Http.Results/tools/ResultsOfTGenerator +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; @@ -20,7 +21,7 @@ namespace Microsoft.AspNetCore.Http.HttpResults; /// /// The first result type. /// The second result type. -public sealed class Results : IResult, INestedHttpResult, IEndpointMetadataProvider +public sealed class Results<[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult1, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult2> : IResult, INestedHttpResult, IEndpointMetadataProvider where TResult1 : IResult where TResult2 : IResult { @@ -83,7 +84,7 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi /// The first result type. /// The second result type. /// The third result type. -public sealed class Results : IResult, INestedHttpResult, IEndpointMetadataProvider +public sealed class Results<[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult1, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult2, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult3> : IResult, INestedHttpResult, IEndpointMetadataProvider where TResult1 : IResult where TResult2 : IResult where TResult3 : IResult @@ -155,7 +156,7 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi /// The second result type. /// The third result type. /// The fourth result type. -public sealed class Results : IResult, INestedHttpResult, IEndpointMetadataProvider +public sealed class Results<[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult1, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult2, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult3, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult4> : IResult, INestedHttpResult, IEndpointMetadataProvider where TResult1 : IResult where TResult2 : IResult where TResult3 : IResult @@ -236,7 +237,7 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi /// The third result type. /// The fourth result type. /// The fifth result type. -public sealed class Results : IResult, INestedHttpResult, IEndpointMetadataProvider +public sealed class Results<[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult1, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult2, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult3, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult4, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult5> : IResult, INestedHttpResult, IEndpointMetadataProvider where TResult1 : IResult where TResult2 : IResult where TResult3 : IResult @@ -326,7 +327,7 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi /// The fourth result type. /// The fifth result type. /// The sixth result type. -public sealed class Results : IResult, INestedHttpResult, IEndpointMetadataProvider +public sealed class Results<[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult1, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult2, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult3, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult4, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult5, [DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult6> : IResult, INestedHttpResult, IEndpointMetadataProvider where TResult1 : IResult where TResult2 : IResult where TResult3 : IResult diff --git a/src/Http/Http.Results/src/ResultsOfTHelper.cs b/src/Http/Http.Results/src/ResultsOfTHelper.cs index a68f55c6dfee..71eefb826714 100644 --- a/src/Http/Http.Results/src/ResultsOfTHelper.cs +++ b/src/Http/Http.Results/src/ResultsOfTHelper.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; @@ -9,13 +12,48 @@ namespace Microsoft.AspNetCore.Http; internal static class ResultsOfTHelper { + public const DynamicallyAccessedMemberTypes RequireMethods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; private static readonly MethodInfo PopulateMetadataMethod = typeof(ResultsOfTHelper).GetMethod(nameof(PopulateMetadata), BindingFlags.Static | BindingFlags.NonPublic)!; - public static void PopulateMetadataIfTargetIsIEndpointMetadataProvider(MethodInfo method, EndpointBuilder builder) + // TODO: Improve calling static interface method with reflection + // https://github.com/dotnet/aspnetcore/issues/46267 + public static void PopulateMetadataIfTargetIsIEndpointMetadataProvider<[DynamicallyAccessedMembers(RequireMethods)] TTarget>(MethodInfo method, EndpointBuilder builder) { if (typeof(IEndpointMetadataProvider).IsAssignableFrom(typeof(TTarget))) { - PopulateMetadataMethod.MakeGenericMethod(typeof(TTarget)).Invoke(null, new object[] { method, builder }); + var parameters = new object[] { method, builder }; + + if (RuntimeFeature.IsDynamicCodeSupported) + { + InvokeGenericPopulateMetadata(parameters); + } + else + { + // Prioritize explicit implementation. + var populateMetadataMethod = typeof(TTarget).GetMethod("Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata", BindingFlags.Static | BindingFlags.NonPublic); + if (populateMetadataMethod is null) + { + populateMetadataMethod = typeof(TTarget).GetMethod("PopulateMetadata", BindingFlags.Static | BindingFlags.Public); + } + // Method won't be found if it is from a default interface implementation. + // Improve with https://github.com/dotnet/aspnetcore/issues/46267 + if (populateMetadataMethod is null) + { + throw new InvalidOperationException($"Couldn't populate metadata for {typeof(TTarget).Name}. PopulateMetadata must by defined on the result type. A default interface implementation isn't supported with AOT."); + } + Debug.Assert(populateMetadataMethod != null, $"Couldn't find PopulateMetadata method on {typeof(TTarget)}."); + + populateMetadataMethod.Invoke(null, BindingFlags.DoNotWrapExceptions, binder: null, parameters, culture: null); + } + } + + // TODO: Remove IL3050 suppress when https://github.com/dotnet/linker/issues/2715 is complete. + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Validated with IsDynamicCodeSupported check.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", + Justification = "The call to MakeGenericMethod is safe due to the fact that PopulateMetadata does not have a DynamicallyAccessMembers attribute and TTarget is annotated to preserve all methods to preserve the PopulateMetadata method.")] + static void InvokeGenericPopulateMetadata(object[] parameters) + { + PopulateMetadataMethod.MakeGenericMethod(typeof(TTarget)).Invoke(null, parameters); } } diff --git a/src/Http/Http.Results/src/TypedResults.cs b/src/Http/Http.Results/src/TypedResults.cs index c716a69a6d66..9093a346ec25 100644 --- a/src/Http/Http.Results/src/TypedResults.cs +++ b/src/Http/Http.Results/src/TypedResults.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -139,9 +140,7 @@ public static ContentHttpResult Text(string? content, string? contentType, Encod /// The content type (MIME type). /// The status code to return. /// The created object for the response. -#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. public static Utf8ContentHttpResult Text(ReadOnlySpan utf8Content, string? contentType = null, int? statusCode = null) -#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. => new Utf8ContentHttpResult(utf8Content, contentType, statusCode); /// @@ -159,9 +158,7 @@ public static Utf8ContentHttpResult Text(ReadOnlySpan utf8Content, string? /// The content encoding. /// The status code to return. /// The created object for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static ContentHttpResult Text(string? content, string? contentType = null, Encoding? contentEncoding = null, int? statusCode = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { MediaTypeHeaderValue? mediaTypeHeaderValue = null; if (contentType is not null) @@ -216,9 +213,7 @@ public static JsonHttpResult Json(TValue? data, JsonSerializerOp /// The of when the file was last modified. /// The associated with the file. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileContentHttpResult File( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters byte[] fileContents, string? contentType = null, string? fileDownloadName = null, @@ -253,9 +248,7 @@ public static FileContentHttpResult File( /// The of when the file was last modified. /// The associated with the file. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileContentHttpResult Bytes( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters byte[] contents, string? contentType = null, string? fileDownloadName = null, @@ -288,9 +281,7 @@ public static FileContentHttpResult Bytes( /// The of when the file was last modified. /// The associated with the file. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileContentHttpResult Bytes( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters ReadOnlyMemory contents, string? contentType = null, string? fileDownloadName = null, @@ -327,9 +318,7 @@ public static FileContentHttpResult Bytes( /// and perform conditional requests. /// Set to true to enable range requests processing. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileStreamHttpResult File( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters Stream fileStream, string? contentType = null, string? fileDownloadName = null, @@ -370,9 +359,7 @@ public static FileStreamHttpResult File( /// and perform conditional requests. /// Set to true to enable range requests processing. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileStreamHttpResult Stream( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters Stream stream, string? contentType = null, string? fileDownloadName = null, @@ -410,9 +397,7 @@ public static FileStreamHttpResult Stream( /// and perform conditional requests. /// Set to true to enable range requests processing. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static FileStreamHttpResult Stream( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters PipeReader pipeReader, string? contentType = null, string? fileDownloadName = null, @@ -446,9 +431,7 @@ public static FileStreamHttpResult Stream( /// The to be configure the ETag response header /// and perform conditional requests. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static PushStreamHttpResult Stream( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters Func streamWriterCallback, string? contentType = null, string? fileDownloadName = null, @@ -479,9 +462,7 @@ public static PushStreamHttpResult Stream( /// The associated with the file. /// Set to true to enable range requests processing. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static PhysicalFileHttpResult PhysicalFile( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters string path, string? contentType = null, string? fileDownloadName = null, @@ -517,9 +498,7 @@ public static PhysicalFileHttpResult PhysicalFile( /// The associated with the file. /// Set to true to enable range requests processing. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static VirtualFileHttpResult VirtualFile( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters string path, string? contentType = null, string? fileDownloadName = null, @@ -602,6 +581,7 @@ public static RedirectHttpResult LocalRedirect([StringSyntax(StringSyntaxAttribu /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. /// The fragment to add to the URL. /// The created for the response. + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static RedirectToRouteHttpResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) => new( routeName: routeName, @@ -727,6 +707,13 @@ public static ProblemHttpResult Problem( Type = type, }; + CopyExtensions(extensions, problemDetails); + + return new(problemDetails); + } + + private static void CopyExtensions(IDictionary? extensions, ProblemDetails problemDetails) + { if (extensions is not null) { foreach (var extension in extensions) @@ -734,8 +721,6 @@ public static ProblemHttpResult Problem( problemDetails.Extensions.Add(extension); } } - - return new(problemDetails); } /// @@ -779,13 +764,7 @@ public static ValidationProblem ValidationProblem( problemDetails.Title = title ?? problemDetails.Title; - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions.Add(extension); - } - } + CopyExtensions(extensions, problemDetails); return new(problemDetails); } @@ -849,9 +828,8 @@ public static Created Created(Uri? uri, TValue? value) /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static CreatedAtRoute CreatedAtRoute(string? routeName = null, object? routeValues = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters => new(routeName, routeValues); /// @@ -862,9 +840,8 @@ public static CreatedAtRoute CreatedAtRoute(string? routeName = null, object? ro /// The route data to use for generating the URL. /// The value to be included in the HTTP response body. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static CreatedAtRoute CreatedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters => new(routeName, routeValues, value); /// @@ -917,9 +894,8 @@ public static Accepted Accepted(Uri uri, TValue? value) /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static AcceptedAtRoute AcceptedAtRoute(string? routeName = null, object? routeValues = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters => new(routeName, routeValues); /// @@ -930,9 +906,8 @@ public static AcceptedAtRoute AcceptedAtRoute(string? routeName = null, object? /// The route data to use for generating the URL. /// The value to be included in the HTTP response body. /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + [RequiresUnreferencedCode(RouteValueDictionaryTrimmerWarning.Warning)] public static AcceptedAtRoute AcceptedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters => new(routeName, routeValues, value); /// diff --git a/src/Http/Http.Results/test/ResultsOfTHelperTests.cs b/src/Http/Http.Results/test/ResultsOfTHelperTests.cs new file mode 100644 index 000000000000..90b5b1d3a977 --- /dev/null +++ b/src/Http/Http.Results/test/ResultsOfTHelperTests.cs @@ -0,0 +1,139 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class ResultsOfTHelperTests +{ + [ConditionalTheory] + [RemoteExecutionSupported] + [InlineData(true)] + [InlineData(false)] + public void PopulateMetadataIfTargetIsIEndpointMetadataProvider_PublicMethod_Called(bool isDynamicCodeSupported) + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", isDynamicCodeSupported.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + var metadata = GetMetadata(); + + Assert.Single(metadata); + }, options); + } + + [ConditionalTheory] + [RemoteExecutionSupported] + [InlineData(true)] + [InlineData(false)] + public void PopulateMetadataIfTargetIsIEndpointMetadataProvider_ExplicitMethod_Called(bool isDynamicCodeSupported) + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", isDynamicCodeSupported.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + var metadata = GetMetadata(); + + Assert.Single(metadata); + }, options); + } + + [ConditionalTheory] + [RemoteExecutionSupported] + [InlineData(true)] + [InlineData(false)] + public void PopulateMetadataIfTargetIsIEndpointMetadataProvider_ExplicitAndPublicMethod_ExplicitCalled(bool isDynamicCodeSupported) + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", isDynamicCodeSupported.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + var metadata = GetMetadata(); + + Assert.Single(metadata); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void PopulateMetadataIfTargetIsIEndpointMetadataProvider_DefaultInterfaceMethod_NoDynamicCode_Throws() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Improve with https://github.com/dotnet/aspnetcore/issues/46267 + Assert.Throws(() => GetMetadata()); + }, options); + } + + private static IList GetMetadata() + { + var methodInfo = typeof(ResultsOfTHelperTests).GetMethod(nameof(GetMetadata), BindingFlags.NonPublic | BindingFlags.Static); + var endpointBuilder = new TestEndpointBuilder(); + + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider( + methodInfo, + endpointBuilder); + + return endpointBuilder.Metadata; + } + + private class TestEndpointBuilder : EndpointBuilder + { + public override Endpoint Build() + { + throw new NotImplementedException(); + } + } + + private class PublicMethodEndpointMetadataProvider : IEndpointMetadataProvider + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add("Called"); + } + } + + private class ExplicitMethodEndpointMetadataProvider : IEndpointMetadataProvider + { + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add("Called"); + } + } + + private class ExplicitAndPublicMethodEndpointMetadataProvider : IEndpointMetadataProvider + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + throw new Exception("Shouldn't reach here."); + } + + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add("Called"); + } + } + + private interface IMyEndpointMetadataProvider : IEndpointMetadataProvider + { + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add("Called"); + } + } + + private class DefaultInterfaceMethodEndpointMetadataProvider : IMyEndpointMetadataProvider + { + } +} diff --git a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs index 6dd187576438..6755ce04712f 100644 --- a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs +++ b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs @@ -60,6 +60,7 @@ static void GenerateClassFile(string classFilePath, int typeArgCount, bool inter writer.WriteLine(); // Usings + writer.WriteLine("using System.Diagnostics.CodeAnalysis;"); writer.WriteLine("using System.Reflection;"); writer.WriteLine("using Microsoft.AspNetCore.Builder;"); writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;"); @@ -97,7 +98,7 @@ static void GenerateClassFile(string classFilePath, int typeArgCount, bool inter // Type args for (int j = 1; j <= i; j++) { - writer.Write($"TResult{j}"); + writer.Write($"[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult{j}"); if (j != i) { writer.Write(", "); diff --git a/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs b/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs index c875e522dcf4..be520e200ac6 100644 --- a/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs +++ b/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Routing; diff --git a/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs b/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs index 4a26f2036a86..6871eed9dd1b 100644 --- a/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs +++ b/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Routing; diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index bfa84d35edb5..068c308b367c 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs index e88f09aba12e..0a87d2a7b434 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing.Constraints; namespace Microsoft.AspNetCore.Routing.Patterns; diff --git a/src/Http/Routing/src/RouteValueDictionaryTrimmerWarning.cs b/src/Shared/RouteValueDictionaryTrimmerWarning.cs similarity index 73% rename from src/Http/Routing/src/RouteValueDictionaryTrimmerWarning.cs rename to src/Shared/RouteValueDictionaryTrimmerWarning.cs index 020ad001f110..67b7d866b006 100644 --- a/src/Http/Routing/src/RouteValueDictionaryTrimmerWarning.cs +++ b/src/Shared/RouteValueDictionaryTrimmerWarning.cs @@ -1,10 +1,10 @@ // 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.Routing; +namespace Microsoft.AspNetCore.Internal; internal static class RouteValueDictionaryTrimmerWarning { public const string Warning = "This API may perform reflection on supplied parameters which may be trimmed if not referenced directly. " + - "Consider using a different overload to avoid this issue."; + "Initialize a RouteValueDictionary with route values to avoid this issue."; } diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 5b6be32951d9..1d94de43ae44 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -33,6 +33,7 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", @@ -115,4 +116,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file