From f8551939a1dfb29b7bbd323121694f6ced44979f Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Tue, 10 Aug 2021 10:47:51 -0700 Subject: [PATCH 01/29] add support for request media types --- .../src/Metadata/AcceptsMetadata.cs | 33 +++++++++ .../src/Metadata/IAcceptsMetadata.cs | 19 +++++ .../src/PublicAPI.Unshipped.txt | 5 ++ .../MvcCoreServiceCollectionExtensions.cs | 3 +- ...tcherPolicy.cs => AcceptsMatcherPolicy.cs} | 17 ++--- .../src/Routing/ActionEndpointFactory.cs | 5 +- .../Mvc.Core/src/Routing/ConsumesMetadata.cs | 24 ------- .../Mvc.Core/src/Routing/IConsumesMetadata.cs | 13 ---- .../MvcCoreServiceCollectionExtensionsTest.cs | 3 +- ...icyTest.cs => AcceptsMatcherPolicyTest.cs} | 69 ++++++++++--------- 10 files changed, 108 insertions(+), 83 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs create mode 100644 src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs rename src/Mvc/Mvc.Core/src/Routing/{ConsumesMatcherPolicy.cs => AcceptsMatcherPolicy.cs} (96%) delete mode 100644 src/Mvc/Mvc.Core/src/Routing/ConsumesMetadata.cs delete mode 100644 src/Mvc/Mvc.Core/src/Routing/IConsumesMetadata.cs rename src/Mvc/Mvc.Core/test/Routing/{ConsumesMatcherPolicyTest.cs => AcceptsMatcherPolicyTest.cs} (85%) diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs new file mode 100644 index 000000000000..f4d35bb665d9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Metadata that specifies the supported request content types. + /// + public sealed class AcceptsMetadata : IAcceptsMetadata + { + /// + /// Creates a new instance of . + /// + public AcceptsMetadata(string[] contentTypes) + { + if (contentTypes == null) + { + throw new ArgumentNullException(nameof(contentTypes)); + } + + ContentTypes = contentTypes; + } + + /// + /// Gets the supported request content types. + /// + public IReadOnlyList ContentTypes { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs new file mode 100644 index 000000000000..f0abb1458a8f --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface for accepting request media types. + /// + public interface IAcceptsMetadata + { + /// + /// Gets a list of request content types. + /// + IReadOnlyList ContentTypes { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 9338ae940214..0568a7d22798 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -7,6 +7,11 @@ *REMOVED*abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string! Microsoft.AspNetCore.Http.IResult Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 27157062d7c8..a93572ca4b6c 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -176,7 +177,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddEnumerable(ServiceDescriptor.Transient()); // Policies for Endpoints - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); // diff --git a/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs similarity index 96% rename from src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs rename to src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs index 1584042c593e..d336077fae3f 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs @@ -7,13 +7,14 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; namespace Microsoft.AspNetCore.Mvc.Routing { - internal class ConsumesMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy + internal class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type"; internal const string AnyContentType = "*/*"; @@ -51,7 +52,7 @@ bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoint private bool AppliesToEndpointsCore(IReadOnlyList endpoints) { - return endpoints.Any(e => e.Metadata.GetMetadata()?.ContentTypes.Count > 0); + return endpoints.Any(e => e.Metadata.GetMetadata()?.ContentTypes.Count > 0); } public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) @@ -75,7 +76,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) // We do this check first for consistency with how 415 is implemented for the graph version // of this code. We still want to know if any endpoints in this set require an a ContentType // even if those endpoints are already invalid - hence the null check. - var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); + var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); if (metadata == null || metadata.ContentTypes.Count == 0) { // Can match any content type. @@ -107,7 +108,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) } var contentType = httpContext.Request.ContentType; - var mediaType = string.IsNullOrEmpty(contentType) ? (MediaType?)null : new MediaType(contentType); + var mediaType = string.IsNullOrEmpty(contentType) ? null : new MediaType(contentType); var matched = false; for (var j = 0; j < metadata.ContentTypes.Count; j++) @@ -171,7 +172,7 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) for (var i = 0; i < endpoints.Count; i++) { var endpoint = endpoints[i]; - var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes; + var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes; if (contentTypes == null || contentTypes.Count == 0) { contentTypes = new string[] { AnyContentType, }; @@ -193,7 +194,7 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) for (var i = 0; i < endpoints.Count; i++) { var endpoint = endpoints[i]; - var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes ?? Array.Empty(); + var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes ?? Array.Empty(); if (contentTypes.Count == 0) { // OK this means that this endpoint matches *all* content methods. @@ -344,9 +345,9 @@ private int GetScore(in MediaType mediaType) } } - private class ConsumesMetadataEndpointComparer : EndpointMetadataComparer + private class ConsumesMetadataEndpointComparer : EndpointMetadataComparer { - protected override int CompareMetadata(IConsumesMetadata? x, IConsumesMetadata? y) + protected override int CompareMetadata(IAcceptsMetadata? x, IAcceptsMetadata? y) { // Ignore the metadata if it has an empty list of content types. return base.CompareMetadata( diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs index c0b3d89e6b67..0f825940ca24 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Filters; @@ -378,9 +379,9 @@ private static void AddActionDataToBuilder( builder.Metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods)); } else if (actionConstraint is ConsumesAttribute consumesAttribute && - !builder.Metadata.OfType().Any()) + !builder.Metadata.OfType().Any()) { - builder.Metadata.Add(new ConsumesMetadata(consumesAttribute.ContentTypes.ToArray())); + builder.Metadata.Add(new AcceptsMetadata(consumesAttribute.ContentTypes.ToArray())); } else if (!builder.Metadata.Contains(actionConstraint)) { diff --git a/src/Mvc/Mvc.Core/src/Routing/ConsumesMetadata.cs b/src/Mvc/Mvc.Core/src/Routing/ConsumesMetadata.cs deleted file mode 100644 index 101dd3a26e76..000000000000 --- a/src/Mvc/Mvc.Core/src/Routing/ConsumesMetadata.cs +++ /dev/null @@ -1,24 +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; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Mvc.Routing -{ - internal class ConsumesMetadata : IConsumesMetadata - { - public ConsumesMetadata(string[] contentTypes) - { - if (contentTypes == null) - { - throw new ArgumentNullException(nameof(contentTypes)); - } - - ContentTypes = contentTypes; - } - - public IReadOnlyList ContentTypes { get; } - } -} diff --git a/src/Mvc/Mvc.Core/src/Routing/IConsumesMetadata.cs b/src/Mvc/Mvc.Core/src/Routing/IConsumesMetadata.cs deleted file mode 100644 index 01aa87207835..000000000000 --- a/src/Mvc/Mvc.Core/src/Routing/IConsumesMetadata.cs +++ /dev/null @@ -1,13 +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.Collections.Generic; - -namespace Microsoft.AspNetCore.Mvc.Routing -{ - internal interface IConsumesMetadata - { - IReadOnlyList ContentTypes { get; } - } -} diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 47dcd3012705..a646af188329 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -324,7 +325,7 @@ private Dictionary MultiRegistrationServiceTypes typeof(MatcherPolicy), new Type[] { - typeof(ConsumesMatcherPolicy), + typeof(AcceptsMatcherPolicy), typeof(ActionConstraintMatcherPolicy), typeof(DynamicControllerEndpointMatcherPolicy), } diff --git a/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs b/src/Mvc/Mvc.Core/test/Routing/AcceptsMatcherPolicyTest.cs similarity index 85% rename from src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs rename to src/Mvc/Mvc.Core/test/Routing/AcceptsMatcherPolicyTest.cs index 8575e3d9ed3d..a620dc1d05e4 100644 --- a/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/AcceptsMatcherPolicyTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing { // There are some unit tests here for the IEndpointSelectorPolicy implementation. // The INodeBuilderPolicy implementation is well-tested by functional tests. - public class ConsumesMatcherPolicyTest + public class AcceptsMatcherPolicyTest { [Fact] public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse() @@ -38,7 +39,7 @@ public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_Re // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; var policy = (INodeBuilderPolicy)CreatePolicy(); @@ -56,8 +57,8 @@ public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasContentTypes_Return // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", })), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; var policy = (INodeBuilderPolicy)CreatePolicy(); @@ -75,8 +76,8 @@ public void INodeBuilderPolicy_AppliesToEndpoints_WithDynamicMetadata_ReturnsFal // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty()), new DynamicEndpointMetadata()), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", })), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; var policy = (INodeBuilderPolicy)CreatePolicy(); @@ -109,7 +110,7 @@ public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutContentTyp // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty()), new DynamicEndpointMetadata()), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), }; var policy = (IEndpointSelectorPolicy)CreatePolicy(); @@ -127,8 +128,8 @@ public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasContentTypes_R // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty()), new DynamicEndpointMetadata()), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", })), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; var policy = (IEndpointSelectorPolicy)CreatePolicy(); @@ -146,8 +147,8 @@ public void IEndpointSelectorPolicy_AppliesToEndpoints_WithoutDynamicMetadata_Re // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", })), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; var policy = (IEndpointSelectorPolicy)CreatePolicy(); @@ -167,11 +168,11 @@ public void GetEdges_GroupsByContentType() { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", "application/*+json", })), - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/xml", "application/*+xml", })), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/*", })), - CreateEndpoint("/", new ConsumesMetadata(new[]{ "*/*", })), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", "application/*+json", })), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/xml", "application/*+xml", })), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/*", })), + CreateEndpoint("/", new AcceptsMetadata(new[]{ "*/*", })), }; var policy = CreatePolicy(); @@ -227,9 +228,9 @@ public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint() { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", "application/*+json", })), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/xml", "application/*+xml", })), - CreateEndpoint("/", new ConsumesMetadata(new[] { "application/*", })), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", "application/*+json", })), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/xml", "application/*+xml", })), + CreateEndpoint("/", new AcceptsMetadata(new[] { "application/*", })), }; var policy = CreatePolicy(); @@ -248,7 +249,7 @@ public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint() e => { Assert.Equal("*/*", e.State); - Assert.Equal(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName); + Assert.Equal(AcceptsMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName); }, e => { @@ -343,7 +344,7 @@ public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithoutContentTyp // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; var candidates = CreateCandidateSet(endpoints); @@ -364,7 +365,7 @@ public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithoutContentT // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "*/*" })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })), }; var candidates = CreateCandidateSet(endpoints); @@ -412,7 +413,7 @@ public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithAnyContentTyp // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(Array.Empty())), + CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; var candidates = CreateCandidateSet(endpoints); @@ -439,7 +440,7 @@ public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithAnyContentT // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "*/*" })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })), }; var candidates = CreateCandidateSet(endpoints); @@ -466,7 +467,7 @@ public async Task ApplyAsync_EndpointHasSubTypeWildcard_MatchWithValidContentTyp // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "application/*+json", })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "application/*+json", })), }; var candidates = CreateCandidateSet(endpoints); @@ -493,7 +494,7 @@ public async Task ApplyAsync_EndpointHasMultipleContentType_MatchWithValidConten // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "text/xml", "application/xml", })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), }; var candidates = CreateCandidateSet(endpoints); @@ -520,7 +521,7 @@ public async Task ApplyAsync_EndpointDoesNotMatch_Returns415() // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "text/xml", "application/xml", })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), }; var candidates = CreateCandidateSet(endpoints); @@ -548,7 +549,7 @@ public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTy // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "text/xml", "application/xml", })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), CreateEndpoint("/", null) }; @@ -577,8 +578,8 @@ public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTy // Arrange var endpoints = new[] { - CreateEndpoint("/", new ConsumesMetadata(new string[] { "text/xml", "application/xml", })), - CreateEndpoint("/", new ConsumesMetadata(new string[] { "*/*", })) + CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), + CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*", })) }; var candidates = CreateCandidateSet(endpoints); @@ -601,7 +602,7 @@ public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTy Assert.Null(httpContext.GetEndpoint()); } - private static RouteEndpoint CreateEndpoint(string template, ConsumesMetadata consumesMetadata, params object[] more) + private static RouteEndpoint CreateEndpoint(string template, AcceptsMetadata consumesMetadata, params object[] more) { var metadata = new List(); if (consumesMetadata != null) @@ -627,9 +628,9 @@ private static CandidateSet CreateCandidateSet(Endpoint[] endpoints) return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], new int[endpoints.Length]); } - private static ConsumesMatcherPolicy CreatePolicy() + private static AcceptsMatcherPolicy CreatePolicy() { - return new ConsumesMatcherPolicy(); + return new AcceptsMatcherPolicy(); } private class DynamicEndpointMetadata : IDynamicEndpointMetadata From 8133bfffbd90d322e25cf2828588a10811dc1e41 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Tue, 10 Aug 2021 20:58:23 -0700 Subject: [PATCH 02/29] change namespace for acceptsmatcher policy --- .../RoutingServiceCollectionExtensions.cs | 1 + .../src/Matching}/AcceptsMatcherPolicy.cs | 48 +++++++++++-------- .../Matching}/AcceptsMatcherPolicyTest.cs | 3 +- .../MvcCoreServiceCollectionExtensions.cs | 2 +- 4 files changed, 30 insertions(+), 24 deletions(-) rename src/{Mvc/Mvc.Core/src/Routing => Http/Routing/src/Matching}/AcceptsMatcherPolicy.cs (87%) rename src/{Mvc/Mvc.Core/test/Routing => Http/Routing/test/UnitTests/Matching}/AcceptsMatcherPolicyTest.cs (99%) diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index 226b85cebe75..2c0944eec4e4 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -91,6 +91,7 @@ public static IServiceCollection AddRouting(this IServiceCollection services) services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); // // Misc infrastructure diff --git a/src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs similarity index 87% rename from src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs rename to src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index d336077fae3f..67241cb66281 100644 --- a/src/Mvc/Mvc.Core/src/Routing/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -8,11 +8,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Mvc.Routing +namespace Microsoft.AspNetCore.Routing.Matching { internal class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { @@ -108,12 +107,12 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) } var contentType = httpContext.Request.ContentType; - var mediaType = string.IsNullOrEmpty(contentType) ? null : new MediaType(contentType); + var mediaType = string.IsNullOrEmpty(contentType) ? null : new MediaTypeHeaderValue(contentType); var matched = false; for (var j = 0; j < metadata.ContentTypes.Count; j++) { - var candidateMediaType = new MediaType(metadata.ContentTypes[j]); + var candidateMediaType = new MediaTypeHeaderValue(metadata.ContentTypes[j]); if (candidateMediaType.MatchesAllTypes) { // We don't need a 415 response because there's an endpoint that would accept any type. @@ -127,7 +126,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) } // We have a ContentType but it's not a match. - else if (mediaType != null && !mediaType.Value.IsSubsetOf(candidateMediaType)) + else if (mediaType != null && !mediaType.IsSubsetOf(candidateMediaType)) { continue; } @@ -213,13 +212,13 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) foreach (var kvp in edges) { // The edgeKey maps to a possible request header value - var edgeKey = new MediaType(kvp.Key); + var edgeKey = new MediaTypeHeaderValue(kvp.Key); for (var j = 0; j < contentTypes.Count; j++) { var contentType = contentTypes[j]; - var mediaType = new MediaType(contentType); + var mediaType = new MediaTypeHeaderValue(contentType); // Example: 'application/json' is subset of 'application/*' // @@ -293,7 +292,9 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList()); // Policies for Endpoints - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + //services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); // From 17853dd8df03b08b279574cc45bc5668681d5ef5 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Wed, 11 Aug 2021 11:31:18 -0700 Subject: [PATCH 03/29] additional changes --- .../src/RequestDelegateFactory.cs | 19 ++++++++++++------- .../src/RequestDelegateFactoryOptions.cs | 7 +++++++ ...malActionEndpointRouteBuilderExtensions.cs | 9 +++++++++ .../src/Matching/AcceptsMatcherPolicy.cs | 17 ++++++++--------- .../MvcCoreServiceCollectionExtensionsTest.cs | 1 - 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 95c6ac99eb5e..a9eea03d5e0c 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -160,7 +160,7 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func 0 ? CreateParamCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments, factoryContext) : @@ -174,7 +174,7 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func().FirstOrDefault() is { } bodyAttribute) { - return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext); + return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext, options); } else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) { @@ -266,7 +266,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext } } - return BindParameterFromBody(parameter, allowEmpty: false, factoryContext); + return BindParameterFromBody(parameter, allowEmpty: false, factoryContext, options); } } @@ -709,13 +709,18 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext); } - private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) + private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext, RequestDelegateFactoryOptions? options) { if (factoryContext.JsonRequestBodyType is not null) { throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); } + if(options is not null) + { + options.HasBodyParameter = true; + } + var nullability = NullabilityContext.Create(parameter); var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable; diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 6aabd425a1fd..baf0ebacb3cd 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -17,5 +17,12 @@ public sealed class RequestDelegateFactoryOptions /// The list of route parameter names that are specified for this handler. /// public IEnumerable? RouteParameterNames { get; init; } + + /// + /// Check if the reques has a body + /// +#pragma warning disable RS0016 // Add public types and members to the declared API + public bool HasBodyParameter { get; set; } +#pragma warning restore RS0016 // Add public types and members to the declared API } } diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 02addb8e468b..bdddb7c5a37c 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -187,6 +188,14 @@ public static MinimalActionEndpointConventionBuilder Map( // Add delegate attributes as metadata var attributes = action.Method.GetCustomAttributes(); + //Add accepts metadata - Adding two mime types for testing. N + + if(options.HasBodyParameter) + { + builder.Metadata.Add(action.Method); + builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); + } + // This can be null if the delegate is a dynamic method or compiled from an expression tree if (attributes is not null) { diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 67241cb66281..802eef3e891d 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -292,9 +292,8 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList MultiRegistrationServiceTypes typeof(MatcherPolicy), new Type[] { - typeof(AcceptsMatcherPolicy), typeof(ActionConstraintMatcherPolicy), typeof(DynamicControllerEndpointMatcherPolicy), } From a24a181b4db028a7c39248007fb91da7af8f77f0 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Wed, 11 Aug 2021 13:51:08 -0700 Subject: [PATCH 04/29] enable 415 when unsupported content type is provide --- src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs | 2 +- src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs | 2 +- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 4 ++-- src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs | 4 ++-- .../DependencyInjection/MvcCoreServiceCollectionExtensions.cs | 1 - 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs index f4d35bb665d9..74fc5e28eec4 100644 --- a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs @@ -28,6 +28,6 @@ public AcceptsMetadata(string[] contentTypes) /// /// Gets the supported request content types. /// - public IReadOnlyList ContentTypes { get; } + public IEnumerable ContentTypes { get; } } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index f0abb1458a8f..5067c6d02f24 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -14,6 +14,6 @@ public interface IAcceptsMetadata /// /// Gets a list of request content types. /// - IReadOnlyList ContentTypes { get; } + IEnumerable ContentTypes { get; } } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 0568a7d22798..8c48b6db65d8 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,9 +9,9 @@ Microsoft.AspNetCore.Http.IResult Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata -Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 802eef3e891d..af285d63689a 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing.Matching { - internal class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy + internal sealed class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type"; internal const string AnyContentType = "*/*"; @@ -311,7 +311,7 @@ private static int GetNoContentTypeDestination((MediaTypeHeaderValue? mediaType, { var mediaType = destinations[i].mediaType; - if (mediaType is not null && !mediaType.Type.HasValue) + if (mediaType is null || !mediaType.Type.HasValue) { return destinations[i].destination; } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index d49ae89d0d1e..0c9d81fbf9ac 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -177,7 +177,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddEnumerable(ServiceDescriptor.Transient()); // Policies for Endpoints - //services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); // From 2116e6dffe4983d4b0c24fb5a0b73e56cf49b738 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Wed, 11 Aug 2021 16:09:31 -0700 Subject: [PATCH 05/29] add accepts extension method on minimalActions endpoint --- ...nApiEndpointConventionBuilderExtensions.cs | 32 +++++++++++++++++++ src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 1 + 2 files changed, 33 insertions(+) diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 78c9b6a826db..252278165980 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -1,8 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Http.Headers; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Http @@ -120,5 +124,33 @@ public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(t return Produces(builder, statusCode, contentType); } + + + /// + /// Adds the to for all builders + /// produced by . + /// + /// The . + /// The response content type that the endpoint accepts. + /// Additional response content types the endpoint accepts + /// A that can be used to further customize the endpoint. + public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, + string contentType, params string[] additionalContentTypes) + { + + if(string.IsNullOrEmpty(contentType)) + { + contentType = "application/json"; + } + + var allContentTypes = new List() + { + contentType + }; + allContentTypes.AddRange(additionalContentTypes); + + builder.WithMetadata(new AcceptsMetadata(allContentTypes.ToArray())); + return builder; + } } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 02d3d3791fdd..9f9c0c1c966b 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -544,6 +544,7 @@ Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection.Items.get -> Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(string? message) -> void Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor.ContentResultExecutor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory! httpResponseStreamWriterFactory) -> void +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! ~Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector.DefaultOutputFormatterSelector(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileContentResultExecutor.FileContentResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.FileResultExecutorBase(Microsoft.Extensions.Logging.ILogger! logger) -> void From 6700e93a8285d948f812d15bcab2e74e574df59b Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 00:38:12 -0700 Subject: [PATCH 06/29] add IAcceptsMetadata to API description --- .../src/Metadata/AcceptsMetadata.cs | 2 +- .../src/Metadata/IAcceptsMetadata.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 4 +-- .../src/DefaultApiDescriptionProvider.cs | 17 ++++++++++ .../EndpointMetadataApiDescriptionProvider.cs | 20 +++++++++++ ...pointMetadataApiDescriptionProviderTest.cs | 34 +++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs index 74fc5e28eec4..f4d35bb665d9 100644 --- a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs @@ -28,6 +28,6 @@ public AcceptsMetadata(string[] contentTypes) /// /// Gets the supported request content types. /// - public IEnumerable ContentTypes { get; } + public IReadOnlyList ContentTypes { get; } } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index 5067c6d02f24..f0abb1458a8f 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -14,6 +14,6 @@ public interface IAcceptsMetadata /// /// Gets a list of request content types. /// - IEnumerable ContentTypes { get; } + IReadOnlyList ContentTypes { get; } } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 8c48b6db65d8..0568a7d22798 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,9 +9,9 @@ Microsoft.AspNetCore.Http.IResult Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata -Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 5a87ad2089fe..a3bb20805cc9 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; @@ -445,6 +446,22 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList GetAcceptsContentTypes(IReadOnlyList? requestMetadataAttributes) + { + // Walk through all 'filter' attributes in order, and allow each one to see or override + // the results of the previous ones. This is similar to the execution path for content-negotiation. + var contentTypes = new List(); + if (requestMetadataAttributes != null) + { + foreach (var metadataAttribute in requestMetadataAttributes) + { + contentTypes.AddRange(metadataAttribute.ContentTypes); + } + } + + return contentTypes; + } + private static IApiRequestMetadataProvider[]? GetRequestMetadataAttributes(ControllerActionDescriptor action) { if (action.FilterDescriptors == null) diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index c659bf540f59..5ac231c0d6a2 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -123,6 +123,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string } AddSupportedRequestFormats(apiDescription.SupportedRequestFormats, hasJsonBody, routeEndpoint.Metadata); + AddAcceptsRequestFormats(apiDescription.SupportedRequestFormats, routeEndpoint.Metadata); AddSupportedResponseTypes(apiDescription.SupportedResponseTypes, methodInfo.ReturnType, routeEndpoint.Metadata); AddActionDescriptorEndpointMetadata(apiDescription.ActionDescriptor, routeEndpoint.Metadata); @@ -227,6 +228,25 @@ private static void AddSupportedRequestFormats( } } + private static void AddAcceptsRequestFormats( + IList supportedRequestFormats, + EndpointMetadataCollection endpointMetadata) + { + var requestMetadata = endpointMetadata.GetOrderedMetadata(); + var declaredContentTypes = DefaultApiDescriptionProvider.GetAcceptsContentTypes(requestMetadata); + + if (declaredContentTypes.Count > 0) + { + foreach (var contentType in declaredContentTypes) + { + supportedRequestFormats.Add(new ApiRequestFormat + { + MediaType = contentType, + }); + } + } + } + private static void AddSupportedResponseTypes( IList supportedResponseTypes, Type returnType, diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index e390d4c85ef4..9bca178d2be9 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -523,6 +524,39 @@ public void HandleMultipleProduces() }); } + [Fact] + public void HandleAcceptsMetadata() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapPost("/api/todos", () => "") + .Accepts("application/json", "application/xml"); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + provider.OnProvidersExecuted(context); + + // Assert + Assert.Collection( + context.Results.SelectMany(r => r.SupportedRequestFormats), + requestType => + { + Assert.Equal("application/json" , requestType.MediaType); + }, + requestType => + { + Assert.Equal("application/xml", requestType.MediaType); + }); + } + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) { return apiResponseType.ApiResponseFormats From 094ed227414c1470003b591eb96713725f2296ab Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 00:43:59 -0700 Subject: [PATCH 07/29] add empty content type test --- ...pointMetadataApiDescriptionProviderTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 9bca178d2be9..0245a15b1801 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -557,6 +557,35 @@ public void HandleAcceptsMetadata() }); } + [Fact] + public void HandleAcceptsMetadataWhenEmptyReturnsDefaultContentType() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapPost("/api/todos", () => "") + .Accepts(""); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + provider.OnProvidersExecuted(context); + + // Assert + Assert.Collection( + context.Results.SelectMany(r => r.SupportedRequestFormats), + requestType => + { + Assert.Equal("application/json", requestType.MediaType); + }); + } + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) { return apiResponseType.ApiResponseFormats From ab297171d01b5637c4e586cb707e4b4072542b61 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 17:19:25 -0700 Subject: [PATCH 08/29] feat: add types for iacceptmetadata --- .../src/Metadata/AcceptsMetadata.cs | 21 +++++++ .../src/Metadata/IAcceptsMetadata.cs | 5 ++ .../src/PublicAPI.Unshipped.txt | 3 + ...malActionEndpointRouteBuilderExtensions.cs | 1 - .../src/Matching/AcceptsMatcherPolicy.cs | 6 +- ...nApiEndpointConventionBuilderExtensions.cs | 63 +++++++++++++++++-- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 + 7 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs index f4d35bb665d9..01e7b34f012c 100644 --- a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs @@ -25,9 +25,30 @@ public AcceptsMetadata(string[] contentTypes) ContentTypes = contentTypes; } + /// + /// Creates a new instance of with a type. + /// + public AcceptsMetadata(Type? type, string[] contentTypes) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + + if (contentTypes == null) + { + throw new ArgumentNullException(nameof(contentTypes)); + } + + ContentTypes = contentTypes; + } + /// /// Gets the supported request content types. /// public IReadOnlyList ContentTypes { get; } + + + /// + /// Accepts request content types of any shape. + /// + public Type? Type { get; } } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index f0abb1458a8f..fb88711d4ee6 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -15,5 +15,10 @@ public interface IAcceptsMetadata /// Gets a list of request content types. /// IReadOnlyList ContentTypes { get; } + + /// + /// Accepts request content types of any shape. + /// + public Type? Type { get; } } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 0568a7d22798..47f3c6641677 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -8,10 +8,13 @@ Microsoft.AspNetCore.Http.IResult Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(System.Type? type, string![]! contentTypes) -> void Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.Type.get -> System.Type? Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.Type.get -> System.Type? Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index bdddb7c5a37c..5965e46c04ba 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -192,7 +192,6 @@ public static MinimalActionEndpointConventionBuilder Map( if(options.HasBodyParameter) { - builder.Metadata.Add(action.Method); builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); } diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index af285d63689a..c23d122bc7f8 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -76,7 +76,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) // of this code. We still want to know if any endpoints in this set require an a ContentType // even if those endpoints are already invalid - hence the null check. var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); - if (metadata == null || metadata.ContentTypes.Count == 0) + if (metadata == null || metadata.ContentTypes?.Count == 0) { // Can match any content type. needs415Endpoint = false; @@ -93,7 +93,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) // We don't want to return a 415 if any content type could be accepted depending on other parameters. if (metadata != null) { - for (var j = 0; j < metadata.ContentTypes.Count; j++) + for (var j = 0; j < metadata.ContentTypes?.Count; j++) { if (string.Equals("*/*", metadata.ContentTypes[j], StringComparison.Ordinal)) { @@ -110,7 +110,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) var mediaType = string.IsNullOrEmpty(contentType) ? null : new MediaTypeHeaderValue(contentType); var matched = false; - for (var j = 0; j < metadata.ContentTypes.Count; j++) + for (var j = 0; j < metadata.ContentTypes?.Count; j++) { var candidateMediaType = new MediaTypeHeaderValue(metadata.ContentTypes[j]); if (candidateMediaType.MatchesAllTypes) diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 252278165980..8ea7153704eb 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -125,7 +125,52 @@ public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(t return Produces(builder, statusCode, contentType); } +#pragma warning disable CS0419 // Ambiguous reference in cref attribute + /// + /// Adds the to for all builders + /// produced by . + /// + /// The type of the request. + /// The . + /// The request content type. Defaults to "application/json" if empty. + /// Additional response content types the endpoint produces for the supplied status code. + /// A that can be used to further customize the endpoint. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, string? contentType = null, params string[] additionalContentTypes) +#pragma warning restore CS0419 // Ambiguous reference in cref attribute +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + { + Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); + + return builder; + } + + + +#pragma warning disable CS0419 // Ambiguous reference in cref attribute + /// + /// Adds the to for all builders + /// produced by . + /// + /// The . + /// The type of the request. Defaults to null. + /// The response content type that the endpoint accepts. + /// Additional response content types the endpoint accepts + /// A that can be used to further customize the endpoint. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, Type requestType , +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters +#pragma warning restore CS0419 // Ambiguous reference in cref attribute + string? contentType = null, params string[] additionalContentTypes) + { + + builder.WithMetadata(new AcceptsMetadata(requestType, GetAllContentTypes(contentType, additionalContentTypes))); + return builder; + } + + +#pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Adds the to for all builders /// produced by . @@ -135,12 +180,22 @@ public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(t /// Additional response content types the endpoint accepts /// A that can be used to further customize the endpoint. public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, +#pragma warning restore CS0419 // Ambiguous reference in cref attribute string contentType, params string[] additionalContentTypes) { - if(string.IsNullOrEmpty(contentType)) + var allContentTypes = GetAllContentTypes(contentType, additionalContentTypes); + builder.WithMetadata(new AcceptsMetadata(allContentTypes)); + + return builder; + } + + private static string[] GetAllContentTypes(string? contentType, string[] additionalContentTypes) + { + + if (string.IsNullOrEmpty(contentType)) { - contentType = "application/json"; + contentType = "application/json"; } var allContentTypes = new List() @@ -148,9 +203,7 @@ public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionE contentType }; allContentTypes.AddRange(additionalContentTypes); - - builder.WithMetadata(new AcceptsMetadata(allContentTypes.ToArray())); - return builder; + return allContentTypes.ToArray(); } } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 9f9c0c1c966b..72d49d876200 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -544,7 +544,9 @@ Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection.Items.get -> Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(string? message) -> void Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor.ContentResultExecutor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory! httpResponseStreamWriterFactory) -> void +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, System.Type! requestType, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! ~Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector.DefaultOutputFormatterSelector(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileContentResultExecutor.FileContentResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.FileResultExecutorBase(Microsoft.Extensions.Logging.ILogger! logger) -> void From 94fa1a72e81df2d0084bc8c984ed51e62cc2e3c6 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 21:04:31 -0700 Subject: [PATCH 09/29] change requestdelegate factory to return metatdata --- .../src/Metadata/AcceptsMetadata.cs | 5 +- .../src/Metadata/IAcceptsMetadata.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 8 +- .../src/RequestDelegateWithMetadata.cs | 20 +++ .../src/PublicAPI.Unshipped.txt | 4 +- .../src/RequestDelegateFactory.cs | 60 +++++--- .../test/RequestDelegateFactoryTests.cs | 145 ++++++++++++------ ...malActionEndpointRouteBuilderExtensions.cs | 11 +- .../IApiRequestMetadataProvider.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 4 - 10 files changed, 178 insertions(+), 83 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs index 01e7b34f012c..fc1329ceeaed 100644 --- a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs @@ -1,7 +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; using System.Collections.Generic; @@ -30,7 +29,7 @@ public AcceptsMetadata(string[] contentTypes) /// public AcceptsMetadata(Type? type, string[] contentTypes) { - Type = type ?? throw new ArgumentNullException(nameof(type)); + RequestType = type ?? throw new ArgumentNullException(nameof(type)); if (contentTypes == null) { @@ -49,6 +48,6 @@ public AcceptsMetadata(Type? type, string[] contentTypes) /// /// Accepts request content types of any shape. /// - public Type? Type { get; } + public Type? RequestType { get; } } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index fb88711d4ee6..cba34691998b 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -19,6 +19,6 @@ public interface IAcceptsMetadata /// /// Accepts request content types of any shape. /// - public Type? Type { get; } + Type? RequestType { get; } } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 47f3c6641677..c190599602f5 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -11,10 +11,10 @@ Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(System.Type? type, string![]! contentTypes) -> void Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.Type.get -> System.Type? +Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.RequestType.get -> System.Type? Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.Type.get -> System.Type? +Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type? Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata @@ -26,6 +26,10 @@ Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? +Microsoft.AspNetCore.Http.RequestDelegateMetadata +Microsoft.AspNetCore.Http.RequestDelegateMetadata.EndpointMetadata.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.Http.RequestDelegateMetadata.EndpointMetadata.set -> void +Microsoft.AspNetCore.Http.RequestDelegateMetadata.RequestDelegateMetadata() -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string! static Microsoft.AspNetCore.Http.HttpProtocol.IsHttp09(string! protocol) -> bool diff --git a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs new file mode 100644 index 000000000000..7b596d396b3b --- /dev/null +++ b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A Class that represents RequestDelegate metadata. + /// + + public sealed class RequestDelegateMetadata + { + /// + /// List of request delgate metadata + /// + public List EndpointMetadata { get; set; } = new(); + } + +} diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index aac4480a487c..9c3d73bd3eed 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -192,8 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList(th static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders! static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders! static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string? -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate! -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> (Microsoft.AspNetCore.Http.RequestDelegate!, Microsoft.AspNetCore.Http.RequestDelegateMetadata!) +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> (Microsoft.AspNetCore.Http.RequestDelegate!, Microsoft.AspNetCore.Http.RequestDelegateMetadata!) static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void static Microsoft.AspNetCore.Http.SendFileResponseExtensions.SendFileAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, Microsoft.Extensions.FileProviders.IFileInfo! file, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index a9eea03d5e0c..da83a57cedff 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -65,7 +65,7 @@ public static partial class RequestDelegateFactory /// The used to configure the behavior of the handler. /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOptions? options = null) + public static (RequestDelegate, RequestDelegateMetadata) Create(Delegate action, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (action is null) @@ -79,12 +79,22 @@ public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOpti null => null, }; - var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, targetExpression); + var factoryContext = new FactoryContext() + { + ServiceProviderIsService = options?.ServiceProvider?.GetService() + }; - return httpContext => + var requestMetadata = new RequestDelegateMetadata() { - return targetableRequestDelegate(action.Target, httpContext); + EndpointMetadata = factoryContext.Metadata }; + + var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, factoryContext, targetExpression); + + return (httpContext => + { + return targetableRequestDelegate(action.Target, httpContext); + }, requestMetadata); } /// @@ -95,7 +105,7 @@ public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOpti /// The used to configure the behavior of the handler. /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static RequestDelegate Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) + public static (RequestDelegate, RequestDelegateMetadata) Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (methodInfo is null) @@ -108,31 +118,45 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func() + }; + if (targetFactory is null) { if (methodInfo.IsStatic) { - var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression: null); + var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression: null); + var metadata = new RequestDelegateMetadata() + { + EndpointMetadata = factoryContext.Metadata + }; - return httpContext => + return (httpContext => { return untargetableRequestDelegate(null, httpContext); - }; + }, metadata); } targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; } var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); - var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression); + var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression); - return httpContext => + var requestMetadata = new RequestDelegateMetadata() { - return targetableRequestDelegate(targetFactory(httpContext), httpContext); + EndpointMetadata = factoryContext.Metadata }; + + return (httpContext => + { + return targetableRequestDelegate(targetFactory(httpContext), httpContext); + }, requestMetadata); } - private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, Expression? targetExpression) + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) { // Non void return type @@ -150,11 +174,6 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func() - }; - if (options?.RouteParameterNames is { } routeParameterNames) { factoryContext.RouteParameters = new(routeParameterNames); @@ -716,10 +735,8 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); } - if(options is not null) - { - options.HasBodyParameter = true; - } + //TODO: Need to know which mimetypes to add here. + factoryContext.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); var nullability = NullabilityContext.Create(parameter); var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable; @@ -956,6 +973,7 @@ private class FactoryContext public bool UsingTempSourceString { get; set; } public List ExtraLocals { get; } = new(); public List ParamCheckExpressions { get; } = new(); + public List Metadata { get; } = new(); } private static partial class Log diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 881cece5477c..f88e31a9250d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Globalization; using System.Linq.Expressions; using System.Net; @@ -25,6 +26,8 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; +using Xunit.Abstractions; +using static System.Collections.Specialized.BitVector32; namespace Microsoft.AspNetCore.Routing.Internal { @@ -91,7 +94,8 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate) { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -111,7 +115,8 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); - var requestDelegate = RequestDelegateFactory.Create(methodInfo!); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = requestDelegateWithMetadata.Item1; var httpContext = new DefaultHttpContext(); @@ -156,7 +161,8 @@ object GetTarget() return new TestNonStaticActionClass(2); } - var requestDelegate = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); + var requestDelegate = requestDelegateWithMetadata.Item1; var httpContext = new DefaultHttpContext(); @@ -202,7 +208,8 @@ static void TestAction(HttpContext httpContext, [FromRoute] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -229,7 +236,7 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + var requestDelegateWithMetadata = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { if (id is not null) { @@ -238,6 +245,8 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() }, new() { RouteParameterNames = new string[] { "id" } }); + var requestDelegate = requestDelegateWithMetadata.Item1; + httpContext.Request.Query = new QueryCollection(new Dictionary { ["id"] = "42" @@ -255,7 +264,9 @@ public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() Assert.NotNull(methodInfo); - var requestDelegate = RequestDelegateFactory.Create(methodInfo!); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = requestDelegateWithMetadata.Item1; + var context = new DefaultHttpContext(); await requestDelegate(context); @@ -281,7 +292,8 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestOptional); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -293,7 +305,8 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestOptional); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -305,7 +318,8 @@ public async Task RequestDelegatePopulatesFromOptionalStringParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestOptionalString); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptionalString); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -322,7 +336,8 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(TestOptional); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -345,7 +360,8 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -372,7 +388,8 @@ void TestAction([FromRoute] int foo) serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -454,7 +471,8 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -475,7 +493,8 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -494,11 +513,13 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR ["tryParsable"] = "invalid!" }); - var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => + var requestDelegateWithMetadata = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); + var requestDelegate = requestDelegateWithMetadata.Item1; + await requestDelegate(httpContext); Assert.Equal(42, httpContext.Items["tryParsable"]); @@ -557,7 +578,8 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -599,7 +621,8 @@ void TestAction([FromQuery] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Query = query; - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -622,7 +645,8 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -696,7 +720,8 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) }); httpContext.RequestServices = mock.Object; - var requestDelegate = RequestDelegateFactory.Create(action); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -718,7 +743,8 @@ public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -739,7 +765,8 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -763,7 +790,8 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -792,7 +820,8 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new RequestBodyDetectionFeature(true)); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -828,7 +857,8 @@ void TestAction([FromBody] Todo todo) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -901,7 +931,8 @@ public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Dele var httpContext = new DefaultHttpContext(); httpContext.RequestServices = new EmptyServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); + var requestDelegate = requestDelegateWithMetadata.Item1; await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -923,7 +954,8 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt var httpContext = new DefaultHttpContext(); httpContext.RequestServices = requestScoped.ServiceProvider; - var requestDelegate = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -942,7 +974,8 @@ void TestAction(HttpContext httpContext) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -965,7 +998,8 @@ void TestAction(CancellationToken cancellationToken) RequestAborted = cts.Token }; - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -987,7 +1021,8 @@ void TestAction(ClaimsPrincipal user) User = new ClaimsPrincipal() }; - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1006,7 +1041,8 @@ void TestAction(HttpRequest httpRequest) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1025,7 +1061,8 @@ void TestAction(HttpResponse httpResponse) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(TestAction); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1069,7 +1106,8 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1143,7 +1181,8 @@ public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1206,7 +1245,8 @@ public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNul var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1223,7 +1263,8 @@ public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delega var httpContext = new DefaultHttpContext(); httpContext.Response.ContentType = "application/json; charset=utf-8"; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1262,7 +1303,8 @@ public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1303,7 +1345,8 @@ public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1341,7 +1384,8 @@ public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(D var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); Assert.Contains(message, exception.Message); @@ -1386,7 +1430,8 @@ public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1445,7 +1490,8 @@ public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1516,7 +1562,8 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1587,7 +1634,8 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, serviceCollection.AddSingleton(Options.Create(jsonOptions)); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1650,7 +1698,8 @@ public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delega httpContext.RequestServices = services; RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; - var requestDelegate = RequestDelegateFactory.Create(@delegate, options); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate, options); + var requestDelegate = requestDelegateWithMetadata.Item1; if (!isInvalid) { @@ -1694,7 +1743,9 @@ public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allows serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); + var requestDelegate = requestDelegateWithMetadata.Item1; + await requestDelegate(httpContext); @@ -1736,7 +1787,8 @@ public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool prov }); } - var requestDelegate = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); @@ -1765,7 +1817,8 @@ public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability( }); } - var requestDelegate = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = requestDelegateWithMetadata.Item1; await requestDelegate(httpContext); diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 5965e46c04ba..f5166c58a59f 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -171,8 +171,13 @@ public static MinimalActionEndpointConventionBuilder Map( RouteParameterNames = routeParams }; + var requestDelegateWithMetadata = RequestDelegateFactory.Create(action, options); + + var requestDelegate = requestDelegateWithMetadata.Item1; + var metadata = requestDelegateWithMetadata.Item2; + var builder = new RouteEndpointBuilder( - RequestDelegateFactory.Create(action, options), + requestDelegate, pattern, defaultOrder) { @@ -189,8 +194,8 @@ public static MinimalActionEndpointConventionBuilder Map( var attributes = action.Method.GetCustomAttributes(); //Add accepts metadata - Adding two mime types for testing. N - - if(options.HasBodyParameter) + var acceptsMetadata = metadata.EndpointMetadata.Any(m => m is IAcceptsMetadata); + if (acceptsMetadata) { builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); } diff --git a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs index 1df6dc86aca2..bb096cddd408 100644 --- a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs @@ -17,4 +17,4 @@ public interface IApiRequestMetadataProvider : IFilterMetadata /// The void SetContentTypes(MediaTypeCollection contentTypes); } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 8ea7153704eb..fc40d2cbf2fb 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -145,8 +145,6 @@ public static MinimalActionEndpointConventionBuilder Accepts(this Mini return builder; } - - #pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Adds the to for all builders @@ -168,8 +166,6 @@ public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionE return builder; } - - #pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Adds the to for all builders From 8096fd32a5b217210ffd22c1a983816474d666cb Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 21:07:57 -0700 Subject: [PATCH 10/29] clean RequestDelegateFactoryOptions.cs --- .../Http.Extensions/src/RequestDelegateFactoryOptions.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index baf0ebacb3cd..6aabd425a1fd 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -17,12 +17,5 @@ public sealed class RequestDelegateFactoryOptions /// The list of route parameter names that are specified for this handler. /// public IEnumerable? RouteParameterNames { get; init; } - - /// - /// Check if the reques has a body - /// -#pragma warning disable RS0016 // Add public types and members to the declared API - public bool HasBodyParameter { get; set; } -#pragma warning restore RS0016 // Add public types and members to the declared API } } From ce440cb62f7834d5d931977a33a78f6c4d00ca73 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 22:15:15 -0700 Subject: [PATCH 11/29] change request delegate to return requestdelegateresult type --- .../src/PublicAPI.Unshipped.txt | 10 +- .../src/RequestDelegateWithMetadata.cs | 11 ++- .../src/PublicAPI.Unshipped.txt | 4 +- .../src/RequestDelegateFactory.cs | 41 ++++----- .../test/RequestDelegateFactoryTests.cs | 92 +++++++++---------- ...malActionEndpointRouteBuilderExtensions.cs | 9 +- 6 files changed, 83 insertions(+), 84 deletions(-) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index c190599602f5..fd4fcc9a979d 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -26,10 +26,12 @@ Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? -Microsoft.AspNetCore.Http.RequestDelegateMetadata -Microsoft.AspNetCore.Http.RequestDelegateMetadata.EndpointMetadata.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.Http.RequestDelegateMetadata.EndpointMetadata.set -> void -Microsoft.AspNetCore.Http.RequestDelegateMetadata.RequestDelegateMetadata() -> void +Microsoft.AspNetCore.Http.RequestDelegateResult +Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.set -> void +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.set -> void +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult() -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string! static Microsoft.AspNetCore.Http.HttpProtocol.IsHttp09(string! protocol) -> bool diff --git a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs index 7b596d396b3b..df2c068bd34e 100644 --- a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs +++ b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs @@ -6,11 +6,18 @@ namespace Microsoft.AspNetCore.Http { /// - /// A Class that represents RequestDelegate metadata. + /// A Class that represents RequestDelegate with associated metadata. /// - public sealed class RequestDelegateMetadata + public sealed class RequestDelegateResult { + /// + /// A function that can process an HTTP request. + /// + /// A task that represents the completion of request processing. + + public RequestDelegate? RequestDelegate { get; set; } + /// /// List of request delgate metadata /// diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 9c3d73bd3eed..65dc3997cc20 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -192,8 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList(th static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders! static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders! static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string? -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> (Microsoft.AspNetCore.Http.RequestDelegate!, Microsoft.AspNetCore.Http.RequestDelegateMetadata!) -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> (Microsoft.AspNetCore.Http.RequestDelegate!, Microsoft.AspNetCore.Http.RequestDelegateMetadata!) +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void static Microsoft.AspNetCore.Http.SendFileResponseExtensions.SendFileAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, Microsoft.Extensions.FileProviders.IFileInfo! file, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index da83a57cedff..b29ac0b8bd04 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Linq.Expressions; +using System.Net.Http; using System.Reflection; using System.Security.Claims; using Microsoft.AspNetCore.Http.Features; @@ -63,9 +65,9 @@ public static partial class RequestDelegateFactory /// /// A request handler with any number of custom parameters that often produces a response with its return value. /// The used to configure the behavior of the handler. - /// The . + /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static (RequestDelegate, RequestDelegateMetadata) Create(Delegate action, RequestDelegateFactoryOptions? options = null) + public static RequestDelegateResult Create(Delegate action, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (action is null) @@ -84,17 +86,14 @@ public static (RequestDelegate, RequestDelegateMetadata) Create(Delegate action, ServiceProviderIsService = options?.ServiceProvider?.GetService() }; - var requestMetadata = new RequestDelegateMetadata() - { - EndpointMetadata = factoryContext.Metadata - }; - var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, factoryContext, targetExpression); - return (httpContext => + return new RequestDelegateResult() { - return targetableRequestDelegate(action.Target, httpContext); - }, requestMetadata); + EndpointMetadata = factoryContext.Metadata, + RequestDelegate = httpContext => targetableRequestDelegate(action.Target, httpContext) + }; + } /// @@ -105,7 +104,7 @@ public static (RequestDelegate, RequestDelegateMetadata) Create(Delegate action, /// The used to configure the behavior of the handler. /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static (RequestDelegate, RequestDelegateMetadata) Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) + public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (methodInfo is null) @@ -128,15 +127,12 @@ public static (RequestDelegate, RequestDelegateMetadata) Create(MethodInfo metho if (methodInfo.IsStatic) { var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression: null); - var metadata = new RequestDelegateMetadata() - { - EndpointMetadata = factoryContext.Metadata - }; - return (httpContext => + return new RequestDelegateResult() { - return untargetableRequestDelegate(null, httpContext); - }, metadata); + EndpointMetadata = factoryContext.Metadata, + RequestDelegate = httpContext => untargetableRequestDelegate(null, httpContext) + }; } targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; @@ -145,15 +141,12 @@ public static (RequestDelegate, RequestDelegateMetadata) Create(MethodInfo metho var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression); - var requestMetadata = new RequestDelegateMetadata() + return new RequestDelegateResult() { - EndpointMetadata = factoryContext.Metadata + EndpointMetadata = factoryContext.Metadata, + RequestDelegate = httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext) }; - return (httpContext => - { - return targetableRequestDelegate(targetFactory(httpContext), httpContext); - }, requestMetadata); } private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index f88e31a9250d..d9d5bb95a4e8 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -95,7 +95,7 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate) var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -116,7 +116,7 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() new[] { typeof(HttpContext) }); var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; var httpContext = new DefaultHttpContext(); @@ -162,7 +162,7 @@ object GetTarget() } var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; var httpContext = new DefaultHttpContext(); @@ -209,7 +209,7 @@ static void TestAction(HttpContext httpContext, [FromRoute] int value) httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -245,7 +245,7 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() }, new() { RouteParameterNames = new string[] { "id" } }); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; httpContext.Request.Query = new QueryCollection(new Dictionary { @@ -265,7 +265,7 @@ public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() Assert.NotNull(methodInfo); var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; var context = new DefaultHttpContext(); @@ -293,7 +293,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameter() var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -306,7 +306,7 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -319,7 +319,7 @@ public async Task RequestDelegatePopulatesFromOptionalStringParameter() var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptionalString); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -337,7 +337,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -361,7 +361,7 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -389,7 +389,7 @@ void TestAction([FromRoute] int foo) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -472,7 +472,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -494,7 +494,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -518,7 +518,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR httpContext.Items["tryParsable"] = tryParsable; }); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -579,7 +579,7 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -622,7 +622,7 @@ void TestAction([FromQuery] int value) httpContext.Request.Query = query; var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -646,7 +646,7 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -721,7 +721,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) httpContext.RequestServices = mock.Object; var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -744,7 +744,7 @@ public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -766,7 +766,7 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) httpContext.Request.Headers["Content-Length"] = "0"; var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -791,7 +791,7 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) httpContext.Request.Headers["Content-Length"] = "0"; var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -821,7 +821,7 @@ void TestAction([FromBody] Todo todo) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -858,7 +858,7 @@ void TestAction([FromBody] Todo todo) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -932,7 +932,7 @@ public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Dele httpContext.RequestServices = new EmptyServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -955,7 +955,7 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt httpContext.RequestServices = requestScoped.ServiceProvider; var requestDelegateWithMetadata = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -975,7 +975,7 @@ void TestAction(HttpContext httpContext) var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -999,7 +999,7 @@ void TestAction(CancellationToken cancellationToken) }; var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1022,7 +1022,7 @@ void TestAction(ClaimsPrincipal user) }; var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1042,7 +1042,7 @@ void TestAction(HttpRequest httpRequest) var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1062,7 +1062,7 @@ void TestAction(HttpResponse httpResponse) var httpContext = new DefaultHttpContext(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1107,7 +1107,7 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1182,7 +1182,7 @@ public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1246,7 +1246,7 @@ public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNul httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1264,7 +1264,7 @@ public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delega httpContext.Response.ContentType = "application/json; charset=utf-8"; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1304,7 +1304,7 @@ public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1346,7 +1346,7 @@ public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1385,7 +1385,7 @@ public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(D httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); Assert.Contains(message, exception.Message); @@ -1431,7 +1431,7 @@ public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) httpContext.Response.Body = responseBodyStream; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1491,7 +1491,7 @@ public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1563,7 +1563,7 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1635,7 +1635,7 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1699,7 +1699,7 @@ public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delega RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate, options); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; if (!isInvalid) { @@ -1744,7 +1744,7 @@ public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allows httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1788,7 +1788,7 @@ public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool prov } var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); @@ -1818,7 +1818,7 @@ public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability( } var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = requestDelegateWithMetadata.Item1; + var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; await requestDelegate(httpContext); diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index f5166c58a59f..1392612d11d7 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -171,13 +171,10 @@ public static MinimalActionEndpointConventionBuilder Map( RouteParameterNames = routeParams }; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action, options); - - var requestDelegate = requestDelegateWithMetadata.Item1; - var metadata = requestDelegateWithMetadata.Item2; + var requestDelegateResult = RequestDelegateFactory.Create(action, options); var builder = new RouteEndpointBuilder( - requestDelegate, + requestDelegateResult.RequestDelegate!, pattern, defaultOrder) { @@ -194,7 +191,7 @@ public static MinimalActionEndpointConventionBuilder Map( var attributes = action.Method.GetCustomAttributes(); //Add accepts metadata - Adding two mime types for testing. N - var acceptsMetadata = metadata.EndpointMetadata.Any(m => m is IAcceptsMetadata); + var acceptsMetadata = requestDelegateResult.EndpointMetadata.Any(m => m is IAcceptsMetadata); if (acceptsMetadata) { builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); From 74b234b6fa4fd6a8fb6f04c926a1fb8706231a90 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 12 Aug 2021 22:33:44 -0700 Subject: [PATCH 12/29] make apis property init only --- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 6 +++--- .../Http.Abstractions/src/RequestDelegateWithMetadata.cs | 4 ++-- .../Builder/MinimalActionEndpointRouteBuilderExtensions.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index fd4fcc9a979d..69b49fe3e989 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -27,10 +27,10 @@ Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? Microsoft.AspNetCore.Http.RequestDelegateResult -Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.set -> void +Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList? +Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? -Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.set -> void +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.init -> void Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult() -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string! diff --git a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs index df2c068bd34e..bb5644144541 100644 --- a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs +++ b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs @@ -16,12 +16,12 @@ public sealed class RequestDelegateResult /// /// A task that represents the completion of request processing. - public RequestDelegate? RequestDelegate { get; set; } + public RequestDelegate? RequestDelegate { get; init; } /// /// List of request delgate metadata /// - public List EndpointMetadata { get; set; } = new(); + public IReadOnlyList? EndpointMetadata { get; init; } } } diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 1392612d11d7..484bcd4bc153 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -191,8 +191,8 @@ public static MinimalActionEndpointConventionBuilder Map( var attributes = action.Method.GetCustomAttributes(); //Add accepts metadata - Adding two mime types for testing. N - var acceptsMetadata = requestDelegateResult.EndpointMetadata.Any(m => m is IAcceptsMetadata); - if (acceptsMetadata) + var acceptsMetadata = requestDelegateResult.EndpointMetadata?.OfType().FirstOrDefault(); + if (acceptsMetadata is not null) { builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); } From 990be7460f35332d8d208dbb704e7cecfac58c7d Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 13 Aug 2021 09:41:01 -0700 Subject: [PATCH 13/29] adding constructor to requestdelegatefactoryResult --- .../src/PublicAPI.Unshipped.txt | 6 +- .../src/RequestDelegateResult.cs | 34 ++++ .../src/RequestDelegateWithMetadata.cs | 27 --- .../src/RequestDelegateFactory.cs | 25 +-- .../test/RequestDelegateFactoryTests.cs | 186 +++++++++--------- ...malActionEndpointRouteBuilderExtensions.cs | 9 +- .../src/Matching/AcceptsMatcherPolicy.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 5 +- 8 files changed, 141 insertions(+), 153 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/RequestDelegateResult.cs delete mode 100644 src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 69b49fe3e989..08b5060e4403 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -27,11 +27,11 @@ Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? Microsoft.AspNetCore.Http.RequestDelegateResult -Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList? +Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.init -> void -Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate! Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.init -> void -Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult() -> void +Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, System.Collections.Generic.IReadOnlyList! metadata) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string! static Microsoft.AspNetCore.Http.HttpProtocol.IsHttp09(string! protocol) -> bool diff --git a/src/Http/Http.Abstractions/src/RequestDelegateResult.cs b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs new file mode 100644 index 000000000000..31afc40d6ce7 --- /dev/null +++ b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// The result of creating a from a + /// + public sealed class RequestDelegateResult + { + /// + /// Creates a new instance of . + /// + public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList metadata) + { + RequestDelegate = requestDelegate; + EndpointMetadata = metadata; + } + + /// + /// Gets the + /// + /// A task that represents the completion of request processing. + public RequestDelegate RequestDelegate { get; init; } + + /// + /// Gets endpoint metadata inferred from creating the + /// + public IReadOnlyList EndpointMetadata { get; init; } + } + +} diff --git a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs b/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs deleted file mode 100644 index bb5644144541..000000000000 --- a/src/Http/Http.Abstractions/src/RequestDelegateWithMetadata.cs +++ /dev/null @@ -1,27 +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.Threading.Tasks; - -namespace Microsoft.AspNetCore.Http -{ - /// - /// A Class that represents RequestDelegate with associated metadata. - /// - - public sealed class RequestDelegateResult - { - /// - /// A function that can process an HTTP request. - /// - /// A task that represents the completion of request processing. - - public RequestDelegate? RequestDelegate { get; init; } - - /// - /// List of request delgate metadata - /// - public IReadOnlyList? EndpointMetadata { get; init; } - } - -} diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index b29ac0b8bd04..5128e675389f 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; +using static System.Collections.Specialized.BitVector32; namespace Microsoft.AspNetCore.Http { @@ -81,18 +82,14 @@ public static RequestDelegateResult Create(Delegate action, RequestDelegateFacto null => null, }; - var factoryContext = new FactoryContext() + var factoryContext = new FactoryContext { ServiceProviderIsService = options?.ServiceProvider?.GetService() }; var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, factoryContext, targetExpression); - return new RequestDelegateResult() - { - EndpointMetadata = factoryContext.Metadata, - RequestDelegate = httpContext => targetableRequestDelegate(action.Target, httpContext) - }; + return new RequestDelegateResult(httpContext => targetableRequestDelegate(action.Target, httpContext), factoryContext.Metadata); } @@ -117,7 +114,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func() }; @@ -128,11 +125,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func untargetableRequestDelegate(null, httpContext) - }; + return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata); } targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; @@ -141,12 +134,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func targetableRequestDelegate(targetFactory(httpContext), httpContext) - }; - + return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); } private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) @@ -728,7 +716,6 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); } - //TODO: Need to know which mimetypes to add here. factoryContext.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); var nullability = NullabilityContext.Create(parameter); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index d9d5bb95a4e8..238e835fc0e0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -26,8 +26,6 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; -using Xunit.Abstractions; -using static System.Collections.Specialized.BitVector32; namespace Microsoft.AspNetCore.Routing.Internal { @@ -94,8 +92,8 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate) { var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -115,8 +113,8 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = factoryResult.RequestDelegate; var httpContext = new DefaultHttpContext(); @@ -161,8 +159,8 @@ object GetTarget() return new TestNonStaticActionClass(2); } - var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); + var requestDelegate = factoryResult.RequestDelegate; var httpContext = new DefaultHttpContext(); @@ -208,8 +206,8 @@ static void TestAction(HttpContext httpContext, [FromRoute] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -236,7 +234,7 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() { var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { if (id is not null) { @@ -245,7 +243,7 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() }, new() { RouteParameterNames = new string[] { "id" } }); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var requestDelegate = factoryResult.RequestDelegate; httpContext.Request.Query = new QueryCollection(new Dictionary { @@ -264,8 +262,8 @@ public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() Assert.NotNull(methodInfo); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = factoryResult.RequestDelegate; var context = new DefaultHttpContext(); @@ -292,8 +290,8 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -305,8 +303,8 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -318,8 +316,8 @@ public async Task RequestDelegatePopulatesFromOptionalStringParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptionalString); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestOptionalString); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -336,8 +334,8 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -360,8 +358,8 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -388,8 +386,8 @@ void TestAction([FromRoute] int foo) serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -471,8 +469,8 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -493,8 +491,8 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -513,12 +511,12 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR ["tryParsable"] = "invalid!" }); - var requestDelegateWithMetadata = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => + var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -578,8 +576,8 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -621,8 +619,8 @@ void TestAction([FromQuery] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Query = query; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -645,8 +643,8 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -720,8 +718,8 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) }); httpContext.RequestServices = mock.Object; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -743,8 +741,8 @@ public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -765,8 +763,8 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -790,8 +788,8 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -820,8 +818,8 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new RequestBodyDetectionFeature(true)); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -857,8 +855,8 @@ void TestAction([FromBody] Todo todo) httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -931,8 +929,8 @@ public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Dele var httpContext = new DefaultHttpContext(); httpContext.RequestServices = new EmptyServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -954,8 +952,8 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt var httpContext = new DefaultHttpContext(); httpContext.RequestServices = requestScoped.ServiceProvider; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -974,8 +972,8 @@ void TestAction(HttpContext httpContext) var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -998,8 +996,8 @@ void TestAction(CancellationToken cancellationToken) RequestAborted = cts.Token }; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1021,8 +1019,8 @@ void TestAction(ClaimsPrincipal user) User = new ClaimsPrincipal() }; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1041,8 +1039,8 @@ void TestAction(HttpRequest httpRequest) var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1061,8 +1059,8 @@ void TestAction(HttpResponse httpResponse) var httpContext = new DefaultHttpContext(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(TestAction); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1106,8 +1104,8 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1181,8 +1179,8 @@ public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1245,8 +1243,8 @@ public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNul var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1263,8 +1261,8 @@ public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delega var httpContext = new DefaultHttpContext(); httpContext.Response.ContentType = "application/json; charset=utf-8"; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1303,8 +1301,8 @@ public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1345,8 +1343,8 @@ public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1384,8 +1382,8 @@ public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(D var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); Assert.Contains(message, exception.Message); @@ -1430,8 +1428,8 @@ public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1490,8 +1488,8 @@ public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1562,8 +1560,8 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1634,8 +1632,8 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, serviceCollection.AddSingleton(Options.Create(jsonOptions)); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1698,8 +1696,8 @@ public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delega httpContext.RequestServices = services; RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate, options); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate, options); + var requestDelegate = factoryResult.RequestDelegate; if (!isInvalid) { @@ -1743,8 +1741,8 @@ public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allows serviceCollection.AddSingleton(LoggerFactory); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegateWithMetadata = RequestDelegateFactory.Create(@delegate); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1787,8 +1785,8 @@ public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool prov }); } - var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1817,8 +1815,8 @@ public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability( }); } - var requestDelegateWithMetadata = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = requestDelegateWithMetadata.RequestDelegate!; + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 484bcd4bc153..60963734ebe8 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -174,7 +174,7 @@ public static MinimalActionEndpointConventionBuilder Map( var requestDelegateResult = RequestDelegateFactory.Create(action, options); var builder = new RouteEndpointBuilder( - requestDelegateResult.RequestDelegate!, + requestDelegateResult.RequestDelegate, pattern, defaultOrder) { @@ -190,11 +190,10 @@ public static MinimalActionEndpointConventionBuilder Map( // Add delegate attributes as metadata var attributes = action.Method.GetCustomAttributes(); - //Add accepts metadata - Adding two mime types for testing. N - var acceptsMetadata = requestDelegateResult.EndpointMetadata?.OfType().FirstOrDefault(); - if (acceptsMetadata is not null) + //Add add request delegate metadata + foreach(var metadata in requestDelegateResult.EndpointMetadata) { - builder.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); + builder.Metadata.Add(metadata); } // This can be null if the delegate is a dynamic method or compiled from an expression tree diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index c23d122bc7f8..aa3b58ab9e4e 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -348,7 +348,7 @@ private int GetScore(MediaTypeHeaderValue? mediaType) } } - private class ConsumesMetadataEndpointComparer : EndpointMetadataComparer + private sealed class ConsumesMetadataEndpointComparer : EndpointMetadataComparer { protected override int CompareMetadata(IAcceptsMetadata? x, IAcceptsMetadata? y) { diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index fc40d2cbf2fb..5aa2b107f2c2 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -160,8 +160,7 @@ public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionE #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters #pragma warning restore CS0419 // Ambiguous reference in cref attribute string? contentType = null, params string[] additionalContentTypes) - { - + { builder.WithMetadata(new AcceptsMetadata(requestType, GetAllContentTypes(contentType, additionalContentTypes))); return builder; } @@ -179,7 +178,6 @@ public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionE #pragma warning restore CS0419 // Ambiguous reference in cref attribute string contentType, params string[] additionalContentTypes) { - var allContentTypes = GetAllContentTypes(contentType, additionalContentTypes); builder.WithMetadata(new AcceptsMetadata(allContentTypes)); @@ -188,7 +186,6 @@ public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionE private static string[] GetAllContentTypes(string? contentType, string[] additionalContentTypes) { - if (string.IsNullOrEmpty(contentType)) { contentType = "application/json"; From 0097323a1478d44c5cb9eab1c8e82c3df223cf91 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 13 Aug 2021 17:44:44 -0700 Subject: [PATCH 14/29] Fixups --- .../src/Matching/AcceptsMatcherPolicy.cs | 563 ++++++++-------- .../src/Microsoft.AspNetCore.Routing.csproj | 6 +- .../src/Formatters/AcceptHeaderParser.cs | 1 + .../src/Formatters/HttpParseResult.cs | 12 - .../src/Formatters/HttpTokenParsingRules.cs | 270 -------- src/Mvc/Mvc.Core/src/Formatters/MediaType.cs | 184 +----- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 4 +- .../SimpleWithWebApplicationBuilderTests.cs | 43 ++ .../Program.cs | 5 +- src/Shared/MediaType/HttpTokenParsingRule.cs | 277 ++++++++ .../MediaType/ReadOnlyMediaTypeHeaderValue.cs | 625 ++++++++++++++++++ 11 files changed, 1255 insertions(+), 735 deletions(-) delete mode 100644 src/Mvc/Mvc.Core/src/Formatters/HttpParseResult.cs delete mode 100644 src/Mvc/Mvc.Core/src/Formatters/HttpTokenParsingRules.cs create mode 100644 src/Shared/MediaType/HttpTokenParsingRule.cs create mode 100644 src/Shared/MediaType/ReadOnlyMediaTypeHeaderValue.cs diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index aa3b58ab9e4e..b15e6566295b 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -1,400 +1,391 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Routing; -using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { - internal sealed class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy - { - internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type"; - internal const string AnyContentType = "*/*"; + internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type"; + internal const string AnyContentType = "*/*"; - // Run after HTTP methods, but before 'default'. - public override int Order { get; } = -100; + // Run after HTTP methods, but before 'default'. + public override int Order { get; } = -100; - public IComparer Comparer { get; } = new ConsumesMetadataEndpointComparer(); + public IComparer Comparer { get; } = new ConsumesMetadataEndpointComparer(); - bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + { + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (ContainsDynamicEndpoints(endpoints)) - { - return false; - } - - return AppliesToEndpointsCore(endpoints); + throw new ArgumentNullException(nameof(endpoints)); } - bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + if (ContainsDynamicEndpoints(endpoints)) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + return false; + } + + return AppliesToEndpointsCore(endpoints); + } - // When the node contains dynamic endpoints we can't make any assumptions. - return ContainsDynamicEndpoints(endpoints); + bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); } - private bool AppliesToEndpointsCore(IReadOnlyList endpoints) + // When the node contains dynamic endpoints we can't make any assumptions. + return ContainsDynamicEndpoints(endpoints); + } + + private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata()?.ContentTypes.Count > 0); + } + + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (httpContext == null) { - return endpoints.Any(e => e.Metadata.GetMetadata()?.ContentTypes.Count > 0); + throw new ArgumentNullException(nameof(httpContext)); } - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + if (candidates == null) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + throw new ArgumentNullException(nameof(candidates)); + } + + // We want to return a 415 if we eliminated ALL of the currently valid endpoints due to content type + // mismatch. + bool? needs415Endpoint = null; - if (candidates == null) + for (var i = 0; i < candidates.Count; i++) + { + // We do this check first for consistency with how 415 is implemented for the graph version + // of this code. We still want to know if any endpoints in this set require an a ContentType + // even if those endpoints are already invalid - hence the null check. + var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); + if (metadata == null || metadata.ContentTypes?.Count == 0) { - throw new ArgumentNullException(nameof(candidates)); + // Can match any content type. + needs415Endpoint = false; + continue; } - // We want to return a 415 if we eliminated ALL of the currently valid endpoints due to content type - // mismatch. - bool? needs415Endpoint = null; + // Saw a valid endpoint. + needs415Endpoint = needs415Endpoint ?? true; - for (var i = 0; i < candidates.Count; i++) + if (!candidates.IsValidCandidate(i)) { - // We do this check first for consistency with how 415 is implemented for the graph version - // of this code. We still want to know if any endpoints in this set require an a ContentType - // even if those endpoints are already invalid - hence the null check. - var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); - if (metadata == null || metadata.ContentTypes?.Count == 0) + // If the candidate is already invalid, then do a search to see if it has a wildcard content type. + // + // We don't want to return a 415 if any content type could be accepted depending on other parameters. + if (metadata != null) { - // Can match any content type. - needs415Endpoint = false; - continue; - } - - // Saw a valid endpoint. - needs415Endpoint = needs415Endpoint ?? true; - - if (!candidates.IsValidCandidate(i)) - { - // If the candidate is already invalid, then do a search to see if it has a wildcard content type. - // - // We don't want to return a 415 if any content type could be accepted depending on other parameters. - if (metadata != null) + for (var j = 0; j < metadata.ContentTypes?.Count; j++) { - for (var j = 0; j < metadata.ContentTypes?.Count; j++) + if (string.Equals("*/*", metadata.ContentTypes[j], StringComparison.Ordinal)) { - if (string.Equals("*/*", metadata.ContentTypes[j], StringComparison.Ordinal)) - { - needs415Endpoint = false; - break; - } + needs415Endpoint = false; + break; } } - - continue; } - var contentType = httpContext.Request.ContentType; - var mediaType = string.IsNullOrEmpty(contentType) ? null : new MediaTypeHeaderValue(contentType); - - var matched = false; - for (var j = 0; j < metadata.ContentTypes?.Count; j++) - { - var candidateMediaType = new MediaTypeHeaderValue(metadata.ContentTypes[j]); - if (candidateMediaType.MatchesAllTypes) - { - // We don't need a 415 response because there's an endpoint that would accept any type. - needs415Endpoint = false; - } - - // If there's no ContentType, then then can only matched by a wildcard `*/*`. - if (mediaType == null && !candidateMediaType.MatchesAllTypes) - { - continue; - } + continue; + } - // We have a ContentType but it's not a match. - else if (mediaType != null && !mediaType.IsSubsetOf(candidateMediaType)) - { - continue; - } + var contentType = httpContext.Request.ContentType; + var mediaType = string.IsNullOrEmpty(contentType) ? (ReadOnlyMediaTypeHeaderValue?)null : new(contentType); - // We have a ContentType and we accept any value OR we have a ContentType and it's a match. - matched = true; + var matched = false; + for (var j = 0; j < metadata.ContentTypes?.Count; j++) + { + var candidateMediaType = new ReadOnlyMediaTypeHeaderValue(metadata.ContentTypes[j]); + if (candidateMediaType.MatchesAllTypes) + { + // We don't need a 415 response because there's an endpoint that would accept any type. needs415Endpoint = false; - break; } - if (!matched) + // If there's no ContentType, then then can only matched by a wildcard `*/*`. + if (mediaType == null && !candidateMediaType.MatchesAllTypes) { - candidates.SetValidity(i, false); + continue; + } + + // We have a ContentType but it's not a match. + else if (mediaType != null && !mediaType.Value.IsSubsetOf(candidateMediaType)) + { + continue; } + + // We have a ContentType and we accept any value OR we have a ContentType and it's a match. + matched = true; + needs415Endpoint = false; + break; } - if (needs415Endpoint == true) + if (!matched) { - // We saw some endpoints coming in, and we eliminated them all. - httpContext.SetEndpoint(CreateRejectionEndpoint()); + candidates.SetValidity(i, false); } + } + + if (needs415Endpoint == true) + { + // We saw some endpoints coming in, and we eliminated them all. + httpContext.SetEndpoint(CreateRejectionEndpoint()); + } + + return Task.CompletedTask; + } - return Task.CompletedTask; + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); } - public IReadOnlyList GetEdges(IReadOnlyList endpoints) + // The algorithm here is designed to be preserve the order of the endpoints + // while also being relatively simple. Preserving order is important. + + // First, build a dictionary of all of the content-type patterns that are included + // at this node. + // + // For now we're just building up the set of keys. We don't add any endpoints + // to lists now because we don't want ordering problems. + var edges = new Dictionary>(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < endpoints.Count; i++) { - if (endpoints == null) + var endpoint = endpoints[i]; + var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes; + if (contentTypes == null || contentTypes.Count == 0) { - throw new ArgumentNullException(nameof(endpoints)); + contentTypes = new string[] { AnyContentType, }; } - // The algorithm here is designed to be preserve the order of the endpoints - // while also being relatively simple. Preserving order is important. - - // First, build a dictionary of all of the content-type patterns that are included - // at this node. - // - // For now we're just building up the set of keys. We don't add any endpoints - // to lists now because we don't want ordering problems. - var edges = new Dictionary>(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < endpoints.Count; i++) + for (var j = 0; j < contentTypes.Count; j++) { - var endpoint = endpoints[i]; - var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes; - if (contentTypes == null || contentTypes.Count == 0) - { - contentTypes = new string[] { AnyContentType, }; - } + var contentType = contentTypes[j]; - for (var j = 0; j < contentTypes.Count; j++) + if (!edges.ContainsKey(contentType)) { - var contentType = contentTypes[j]; - - if (!edges.ContainsKey(contentType)) - { - edges.Add(contentType, new List()); - } + edges.Add(contentType, new List()); } } + } - // Now in a second loop, add endpoints to these lists. We've enumerated all of - // the states, so we want to see which states this endpoint matches. - for (var i = 0; i < endpoints.Count; i++) + // Now in a second loop, add endpoints to these lists. We've enumerated all of + // the states, so we want to see which states this endpoint matches. + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes ?? Array.Empty(); + if (contentTypes.Count == 0) { - var endpoint = endpoints[i]; - var contentTypes = endpoint.Metadata.GetMetadata()?.ContentTypes ?? Array.Empty(); - if (contentTypes.Count == 0) + // OK this means that this endpoint matches *all* content methods. + // So, loop and add it to all states. + foreach (var kvp in edges) { - // OK this means that this endpoint matches *all* content methods. - // So, loop and add it to all states. - foreach (var kvp in edges) - { - kvp.Value.Add(endpoint); - } + kvp.Value.Add(endpoint); } - else + } + else + { + // OK this endpoint matches specific content types -- we have to loop through edges here + // because content types could either be exact (like 'application/json') or they + // could have wildcards (like 'text/*'). We don't expect wildcards to be especially common + // with consumes, but we need to support it. + foreach (var kvp in edges) { - // OK this endpoint matches specific content types -- we have to loop through edges here - // because content types could either be exact (like 'application/json') or they - // could have wildcards (like 'text/*'). We don't expect wildcards to be especially common - // with consumes, but we need to support it. - foreach (var kvp in edges) + // The edgeKey maps to a possible request header value + var edgeKey = new ReadOnlyMediaTypeHeaderValue(kvp.Key); + + for (var j = 0; j < contentTypes.Count; j++) { - // The edgeKey maps to a possible request header value - var edgeKey = new MediaTypeHeaderValue(kvp.Key); + var contentType = contentTypes[j]; + + var mediaType = new ReadOnlyMediaTypeHeaderValue(contentType); - for (var j = 0; j < contentTypes.Count; j++) + // Example: 'application/json' is subset of 'application/*' + // + // This means that when the request has content-type 'application/json' an endpoint + // what consumes 'application/*' should match. + if (edgeKey.IsSubsetOf(mediaType)) { - var contentType = contentTypes[j]; - - var mediaType = new MediaTypeHeaderValue(contentType); - - // Example: 'application/json' is subset of 'application/*' - // - // This means that when the request has content-type 'application/json' an endpoint - // what consumes 'application/*' should match. - if (edgeKey.IsSubsetOf(mediaType)) - { - kvp.Value.Add(endpoint); - - // It's possible that a ConsumesMetadata defines overlapping wildcards. Don't add an endpoint - // to any edge twice - break; - } + kvp.Value.Add(endpoint); + + // It's possible that a ConsumesMetadata defines overlapping wildcards. Don't add an endpoint + // to any edge twice + break; } } } } + } - // If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an - // endpoint that always returns a 415. - if (!edges.TryGetValue(AnyContentType, out var anyEndpoints)) - { - edges.Add(AnyContentType, new List() + // If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an + // endpoint that always returns a 415. + if (!edges.TryGetValue(AnyContentType, out var anyEndpoints)) + { + edges.Add(AnyContentType, new List() { CreateRejectionEndpoint(), }); - // Add a node to use when there is no request content type. - // When there is no content type we want the policy to no-op - edges.Add(string.Empty, endpoints.ToList()); - } - else - { - // If there is an endpoint that accepts */* then it is also used when there is no content type - edges.Add(string.Empty, anyEndpoints.ToList()); - } + // Add a node to use when there is no request content type. + // When there is no content type we want the policy to no-op + edges.Add(string.Empty, endpoints.ToList()); + } + else + { + // If there is an endpoint that accepts */* then it is also used when there is no content type + edges.Add(string.Empty, anyEndpoints.ToList()); + } - return edges - .Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value)) - .ToArray(); - } + return edges + .Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value)) + .ToArray(); + } + + private Endpoint CreateRejectionEndpoint() + { + return new Endpoint( + (context) => + { + context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + Http415EndpointDisplayName); + } - private Endpoint CreateRejectionEndpoint() + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + if (edges == null) { - return new Endpoint( - (context) => - { - context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; - return Task.CompletedTask; - }, - EndpointMetadataCollection.Empty, - Http415EndpointDisplayName); + throw new ArgumentNullException(nameof(edges)); } - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + // Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they + // are then then execute them in linear order. + var ordered = edges + .Select(e => (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination)) + .OrderBy(e => GetScore(e.mediaType)) + .ToArray(); + + // If any edge matches all content types, then treat that as the 'exit'. This will + // always happen because we insert a 415 endpoint. + for (var i = 0; i < ordered.Length; i++) { - if (edges == null) + if (ordered[i].mediaType.MatchesAllTypes) { - throw new ArgumentNullException(nameof(edges)); + exitDestination = ordered[i].destination; + break; } + } - // Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they - // are then then execute them in linear order. - var ordered = edges - .Select(e => (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination)) - .OrderBy(e => GetScore(e.mediaType)) - .ToArray(); - - // If any edge matches all content types, then treat that as the 'exit'. This will - // always happen because we insert a 415 endpoint. - for (var i = 0; i < ordered.Length; i++) - { + var noContentTypeDestination = GetNoContentTypeDestination(ordered); - if (ordered[i].mediaType is { MatchesAllTypes: true }) - { - exitDestination = ordered[i].destination; - break; - } - } + return new ConsumesPolicyJumpTable(exitDestination, noContentTypeDestination, ordered); + } - var noContentTypeDestination = GetNoContentTypeDestination(ordered); + private static int GetNoContentTypeDestination((ReadOnlyMediaTypeHeaderValue mediaType, int destination)[] destinations) + { + for (var i = 0; i < destinations.Length; i++) + { + var mediaType = destinations[i].mediaType; - return new ConsumesPolicyJumpTable(exitDestination, noContentTypeDestination, ordered); + if (!mediaType.Type.HasValue) + { + return destinations[i].destination; + } } - private static int GetNoContentTypeDestination((MediaTypeHeaderValue? mediaType, int destination)[] destinations) - { - for (var i = 0; i < destinations.Length; i++) - { - var mediaType = destinations[i].mediaType; + throw new InvalidOperationException("Could not find destination for no content type."); + } - if (mediaType is null || !mediaType.Type.HasValue) - { - return destinations[i].destination; - } - } + private static ReadOnlyMediaTypeHeaderValue CreateEdgeMediaType(ref PolicyJumpTableEdge e) + { + var mediaType = (string)e.State; + return !string.IsNullOrEmpty(mediaType) ? new ReadOnlyMediaTypeHeaderValue(mediaType) : default; + } - throw new InvalidOperationException("Could not find destination for no content type."); + private static int GetScore(ReadOnlyMediaTypeHeaderValue mediaType) + { + // Higher score == lower priority - see comments on MediaType. + if (mediaType.MatchesAllTypes) + { + return 4; } - - private static MediaTypeHeaderValue? CreateEdgeMediaType(ref PolicyJumpTableEdge e) + else if (mediaType.MatchesAllSubTypes) + { + return 3; + } + else if (mediaType.MatchesAllSubTypesWithoutSuffix) { - var mediaType = (string)e.State; - return !string.IsNullOrEmpty(mediaType) ? new MediaTypeHeaderValue(mediaType) : default; + return 2; } + else + { + return 1; + } + } - private int GetScore(MediaTypeHeaderValue? mediaType) + private sealed class ConsumesMetadataEndpointComparer : EndpointMetadataComparer + { + protected override int CompareMetadata(IAcceptsMetadata? x, IAcceptsMetadata? y) { - - // Higher score == lower priority - see comments on MediaType. - if (mediaType is not null && mediaType.MatchesAllTypes) - { - return 4; - } - else if (mediaType is not null && mediaType.MatchesAllSubTypes) - { - return 3; - } - else if (mediaType is not null && mediaType.MatchesAllSubTypesWithoutSuffix) - { - return 2; - } - else - { - return 1; - } + // Ignore the metadata if it has an empty list of content types. + return base.CompareMetadata( + x?.ContentTypes.Count > 0 ? x : null, + y?.ContentTypes.Count > 0 ? y : null); } + } - private sealed class ConsumesMetadataEndpointComparer : EndpointMetadataComparer + private sealed class ConsumesPolicyJumpTable : PolicyJumpTable + { + private readonly (ReadOnlyMediaTypeHeaderValue mediaType, int destination)[] _destinations; + private readonly int _exitDestination; + private readonly int _noContentTypeDestination; + + public ConsumesPolicyJumpTable(int exitDestination, int noContentTypeDestination, (ReadOnlyMediaTypeHeaderValue mediaType, int destination)[] destinations) { - protected override int CompareMetadata(IAcceptsMetadata? x, IAcceptsMetadata? y) - { - // Ignore the metadata if it has an empty list of content types. - return base.CompareMetadata( - x?.ContentTypes.Count > 0 ? x : null, - y?.ContentTypes.Count > 0 ? y : null); - } + _exitDestination = exitDestination; + _noContentTypeDestination = noContentTypeDestination; + _destinations = destinations; } - private class ConsumesPolicyJumpTable : PolicyJumpTable + public override int GetDestination(HttpContext httpContext) { - private readonly (MediaTypeHeaderValue? mediaType, int destination)[] _destinations; - private readonly int _exitDestination; - private readonly int _noContentTypeDestination; + var contentType = httpContext.Request.ContentType; - public ConsumesPolicyJumpTable(int exitDestination, int noContentTypeDestination, (MediaTypeHeaderValue? mediaType, int destination)[] destinations) + if (string.IsNullOrEmpty(contentType)) { - _exitDestination = exitDestination; - _noContentTypeDestination = noContentTypeDestination; - _destinations = destinations; + return _noContentTypeDestination; } - public override int GetDestination(HttpContext httpContext) + var requestMediaType = new ReadOnlyMediaTypeHeaderValue(contentType); + var destinations = _destinations; + for (var i = 0; i < destinations.Length; i++) { - var contentType = httpContext.Request.ContentType; - if (string.IsNullOrEmpty(contentType)) + var destination = destinations[i].mediaType; + if (requestMediaType.IsSubsetOf(destination)) { - return _noContentTypeDestination; - } - - var requestMediaType = new MediaTypeHeaderValue(contentType); - var destinations = _destinations; - for (var i = 0; i < destinations.Length; i++) - { - - var destination = destinations[i].mediaType; - if (destination is not null && requestMediaType.IsSubsetOf(destination)) - { - return destinations[i].destination; - } + return destinations[i].destination; } - - return _exitDestination; } + + return _exitDestination; } } } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 4c0c0a1a5443..f409fd886210 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core middleware for routing requests to application logic and for generating links. @@ -23,7 +23,9 @@ Microsoft.AspNetCore.Routing.RouteCollection - + + + diff --git a/src/Mvc/Mvc.Core/src/Formatters/AcceptHeaderParser.cs b/src/Mvc/Mvc.Core/src/Formatters/AcceptHeaderParser.cs index 2928a83f0cd2..befb3721b320 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/AcceptHeaderParser.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/AcceptHeaderParser.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Microsoft.AspNetCore.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters { diff --git a/src/Mvc/Mvc.Core/src/Formatters/HttpParseResult.cs b/src/Mvc/Mvc.Core/src/Formatters/HttpParseResult.cs deleted file mode 100644 index 9969b949d847..000000000000 --- a/src/Mvc/Mvc.Core/src/Formatters/HttpParseResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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.Mvc.Formatters -{ - internal enum HttpParseResult - { - Parsed, - NotParsed, - InvalidFormat, - } -} diff --git a/src/Mvc/Mvc.Core/src/Formatters/HttpTokenParsingRules.cs b/src/Mvc/Mvc.Core/src/Formatters/HttpTokenParsingRules.cs deleted file mode 100644 index df6d7d8e873e..000000000000 --- a/src/Mvc/Mvc.Core/src/Formatters/HttpTokenParsingRules.cs +++ /dev/null @@ -1,270 +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.Diagnostics; -using System.Text; - -namespace Microsoft.AspNetCore.Mvc.Formatters -{ - internal static class HttpTokenParsingRules - { - private static readonly bool[] TokenChars = CreateTokenChars(); - private const int MaxNestedCount = 5; - - internal const char CR = '\r'; - internal const char LF = '\n'; - internal const char SP = ' '; - internal const char Tab = '\t'; - internal const int MaxInt64Digits = 19; - internal const int MaxInt32Digits = 10; - - // iso-8859-1, Western European (ISO) - internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591); - - private static bool[] CreateTokenChars() - { - // token = 1* - // CTL = - - var tokenChars = new bool[128]; // everything is false - - for (var i = 33; i < 127; i++) // skip Space (32) & DEL (127) - { - tokenChars[i] = true; - } - - // remove separators: these are not valid token characters - tokenChars[(byte)'('] = false; - tokenChars[(byte)')'] = false; - tokenChars[(byte)'<'] = false; - tokenChars[(byte)'>'] = false; - tokenChars[(byte)'@'] = false; - tokenChars[(byte)','] = false; - tokenChars[(byte)';'] = false; - tokenChars[(byte)':'] = false; - tokenChars[(byte)'\\'] = false; - tokenChars[(byte)'"'] = false; - tokenChars[(byte)'/'] = false; - tokenChars[(byte)'['] = false; - tokenChars[(byte)']'] = false; - tokenChars[(byte)'?'] = false; - tokenChars[(byte)'='] = false; - tokenChars[(byte)'{'] = false; - tokenChars[(byte)'}'] = false; - - return tokenChars; - } - - internal static bool IsTokenChar(char character) - { - // Must be between 'space' (32) and 'DEL' (127) - if (character > 127) - { - return false; - } - - return TokenChars[character]; - } - - internal static int GetTokenLength(string input, int startIndex) - { - Debug.Assert(input != null); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - while (current < input.Length) - { - if (!IsTokenChar(input[current])) - { - return current - startIndex; - } - current++; - } - return input.Length - startIndex; - } - - internal static int GetWhitespaceLength(string input, int startIndex) - { - Debug.Assert(input != null); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - while (current < input.Length) - { - var c = input[current]; - - if ((c == SP) || (c == Tab)) - { - current++; - continue; - } - - if (c == CR) - { - // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. - if ((current + 2 < input.Length) && (input[current + 1] == LF)) - { - char spaceOrTab = input[current + 2]; - if ((spaceOrTab == SP) || (spaceOrTab == Tab)) - { - current += 3; - continue; - } - } - } - - return current - startIndex; - } - - // All characters between startIndex and the end of the string are LWS characters. - return input.Length - startIndex; - } - - internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) - { - var nestedCount = 0; - return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); - } - - // quoted-pair = "\" CHAR - // CHAR = - internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) - { - Debug.Assert(input != null); - Debug.Assert((startIndex >= 0) && (startIndex < input.Length)); - - length = 0; - - if (input[startIndex] != '\\') - { - return HttpParseResult.NotParsed; - } - - // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) - // If so, check whether the character is in the range 0-127. If not, it's an invalid value. - if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) - { - return HttpParseResult.InvalidFormat; - } - - // We don't care what the char next to '\' is. - length = 2; - return HttpParseResult.Parsed; - } - - // TEXT = - // LWS = [CRLF] 1*( SP | HT ) - // CTL = - // - // Since we don't really care about the content of a quoted string or comment, we're more tolerant and - // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). - // - // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like - // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested - // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) - // is unusual. - private static HttpParseResult GetExpressionLength( - string input, - int startIndex, - char openChar, - char closeChar, - bool supportsNesting, - ref int nestedCount, - out int length) - { - Debug.Assert(input != null); - Debug.Assert((startIndex >= 0) && (startIndex < input.Length)); - - length = 0; - - if (input[startIndex] != openChar) - { - return HttpParseResult.NotParsed; - } - - var current = startIndex + 1; // Start parsing with the character next to the first open-char - while (current < input.Length) - { - // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. - // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. - if ((current + 2 < input.Length) && - (GetQuotedPairLength(input, current, out var quotedPairLength) == HttpParseResult.Parsed)) - { - // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, - // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only - // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). - current = current + quotedPairLength; - continue; - } - - // If we support nested expressions and we find an open-char, then parse the nested expressions. - if (supportsNesting && (input[current] == openChar)) - { - nestedCount++; - try - { - // Check if we exceeded the number of nested calls. - if (nestedCount > MaxNestedCount) - { - return HttpParseResult.InvalidFormat; - } - - var nestedResult = GetExpressionLength( - input, - current, - openChar, - closeChar, - supportsNesting, - ref nestedCount, - out var nestedLength); - - switch (nestedResult) - { - case HttpParseResult.Parsed: - current += nestedLength; // add the length of the nested expression and continue. - break; - - case HttpParseResult.NotParsed: - Debug.Fail("'NotParsed' is unexpected: We started nested expression " + - "parsing, because we found the open-char. So either it's a valid nested " + - "expression or it has invalid format."); - break; - - case HttpParseResult.InvalidFormat: - // If the nested expression is invalid, we can't continue, so we fail with invalid format. - return HttpParseResult.InvalidFormat; - - default: - Debug.Fail("Unknown enum result: " + nestedResult); - break; - } - } - finally - { - nestedCount--; - } - } - - if (input[current] == closeChar) - { - length = current - startIndex + 1; - return HttpParseResult.Parsed; - } - current++; - } - - // We didn't see the final quote, therefore we have an invalid expression string. - return HttpParseResult.InvalidFormat; - } - } -} diff --git a/src/Mvc/Mvc.Core/src/Formatters/MediaType.cs b/src/Mvc/Mvc.Core/src/Formatters/MediaType.cs index b7004ca2dd59..c682d5b4f2da 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/MediaType.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/MediaType.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Text; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Formatters @@ -16,7 +17,7 @@ public readonly struct MediaType { private static readonly StringSegment QualityParameter = new StringSegment("q"); - private readonly MediaTypeParameterParser _parameterParser; + private readonly ReadOnlyMediaTypeHeaderValue _mediaTypeHeaderValue; /// /// Initializes a instance. @@ -67,124 +68,7 @@ public MediaType(string mediaType, int offset, int? length) } } - _parameterParser = default(MediaTypeParameterParser); - - var typeLength = GetTypeLength(mediaType, offset, out var type); - if (typeLength == 0) - { - Type = new StringSegment(); - SubType = new StringSegment(); - SubTypeWithoutSuffix = new StringSegment(); - SubTypeSuffix = new StringSegment(); - return; - } - else - { - Type = type; - } - - var subTypeLength = GetSubtypeLength(mediaType, offset + typeLength, out var subType); - if (subTypeLength == 0) - { - SubType = new StringSegment(); - SubTypeWithoutSuffix = new StringSegment(); - SubTypeSuffix = new StringSegment(); - return; - } - else - { - SubType = subType; - - if (TryGetSuffixLength(subType, out var subtypeSuffixLength)) - { - SubTypeWithoutSuffix = subType.Subsegment(0, subType.Length - subtypeSuffixLength - 1); - SubTypeSuffix = subType.Subsegment(subType.Length - subtypeSuffixLength, subtypeSuffixLength); - } - else - { - SubTypeWithoutSuffix = SubType; - SubTypeSuffix = new StringSegment(); - } - } - - _parameterParser = new MediaTypeParameterParser(mediaType, offset + typeLength + subTypeLength, length); - } - - // All GetXXXLength methods work in the same way. They expect to be on the right position for - // the token they are parsing, for example, the beginning of the media type or the delimiter - // from a previous token, like '/', ';' or '='. - // Each method consumes the delimiter token if any, the leading whitespace, then the given token - // itself, and finally the trailing whitespace. - private static int GetTypeLength(string input, int offset, out StringSegment type) - { - if (offset < 0 || offset >= input.Length) - { - type = default(StringSegment); - return 0; - } - - var current = offset + HttpTokenParsingRules.GetWhitespaceLength(input, offset); - - // Parse the type, i.e. in media type string "/; param1=value1; param2=value2" - var typeLength = HttpTokenParsingRules.GetTokenLength(input, current); - if (typeLength == 0) - { - type = default(StringSegment); - return 0; - } - - type = new StringSegment(input, current, typeLength); - - current += typeLength; - current += HttpTokenParsingRules.GetWhitespaceLength(input, current); - - return current - offset; - } - - private static int GetSubtypeLength(string input, int offset, out StringSegment subType) - { - var current = offset; - - // Parse the separator between type and subtype - if (current < 0 || current >= input.Length || input[current] != '/') - { - subType = default(StringSegment); - return 0; - } - - current++; // skip delimiter. - current += HttpTokenParsingRules.GetWhitespaceLength(input, current); - - var subtypeLength = HttpTokenParsingRules.GetTokenLength(input, current); - if (subtypeLength == 0) - { - subType = default(StringSegment); - return 0; - } - - subType = new StringSegment(input, current, subtypeLength); - - current += subtypeLength; - current += HttpTokenParsingRules.GetWhitespaceLength(input, current); - - return current - offset; - } - - private static bool TryGetSuffixLength(StringSegment subType, out int suffixLength) - { - // Find the last instance of '+', if there is one - var startPos = subType.Offset + subType.Length - 1; - for (var currentPos = startPos; currentPos >= subType.Offset; currentPos--) - { - if (subType.Buffer[currentPos] == '+') - { - suffixLength = startPos - currentPos; - return true; - } - } - - suffixLength = 0; - return false; + _mediaTypeHeaderValue = new ReadOnlyMediaTypeHeaderValue(mediaType, offset, length); } /// @@ -193,12 +77,12 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// /// For the media type "application/json", this property gives the value "application". /// - public StringSegment Type { get; } + public StringSegment Type => _mediaTypeHeaderValue.Type; /// /// Gets whether this matches all types. /// - public bool MatchesAllTypes => Type.Equals("*", StringComparison.OrdinalIgnoreCase); + public bool MatchesAllTypes => _mediaTypeHeaderValue.MatchesAllTypes; /// /// Gets the subtype of the . @@ -207,7 +91,7 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// For the media type "application/vnd.example+json", this property gives the value /// "vnd.example+json". /// - public StringSegment SubType { get; } + public StringSegment SubType => _mediaTypeHeaderValue.SubType; /// /// Gets the subtype of the , excluding any structured syntax suffix. @@ -216,7 +100,7 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// For the media type "application/vnd.example+json", this property gives the value /// "vnd.example". /// - public StringSegment SubTypeWithoutSuffix { get; } + public StringSegment SubTypeWithoutSuffix => _mediaTypeHeaderValue.SubTypeWithoutSuffix; /// /// Gets the structured syntax suffix of the if it has one. @@ -225,7 +109,7 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// For the media type "application/vnd.example+json", this property gives the value /// "json". /// - public StringSegment SubTypeSuffix { get; } + public StringSegment SubTypeSuffix => _mediaTypeHeaderValue.SubTypeSuffix; /// /// Gets whether this matches all subtypes. @@ -236,7 +120,7 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// /// For the media type "application/json", this property is false. /// - public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase); + public bool MatchesAllSubTypes => _mediaTypeHeaderValue.MatchesAllSubTypes; /// /// Gets whether this matches all subtypes, ignoring any structured syntax suffix. @@ -247,17 +131,17 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// /// For the media type "application/vnd.example+json", this property is false. /// - public bool MatchesAllSubTypesWithoutSuffix => SubTypeWithoutSuffix.Equals("*", StringComparison.OrdinalIgnoreCase); + public bool MatchesAllSubTypesWithoutSuffix => _mediaTypeHeaderValue.MatchesAllSubTypesWithoutSuffix; /// /// Gets the of the if it has one. /// - public Encoding? Encoding => GetEncodingFromCharset(GetParameter("charset")); + public Encoding? Encoding => _mediaTypeHeaderValue.Encoding; /// /// Gets the charset parameter of the if it has one. /// - public StringSegment Charset => GetParameter("charset"); + public StringSegment Charset => _mediaTypeHeaderValue.Charset; /// /// Determines whether the current contains a wildcard. @@ -265,15 +149,7 @@ private static bool TryGetSuffixLength(StringSegment subType, out int suffixLeng /// /// true if this contains a wildcard; otherwise false. /// - public bool HasWildcard - { - get - { - return MatchesAllTypes || - MatchesAllSubTypesWithoutSuffix || - GetParameter("*").Equals("*", StringComparison.OrdinalIgnoreCase); - } - } + public bool HasWildcard => _mediaTypeHeaderValue.HasWildcard; /// /// Determines whether the current is a subset of the @@ -284,11 +160,7 @@ public bool HasWildcard /// true if this is a subset of ; otherwise false. /// public bool IsSubsetOf(MediaType set) - { - return MatchesType(set) && - MatchesSubtype(set) && - ContainsAllParameters(set._parameterParser); - } + => _mediaTypeHeaderValue.IsSubsetOf(set._mediaTypeHeaderValue); /// /// Gets the parameter of the media type. @@ -299,9 +171,7 @@ public bool IsSubsetOf(MediaType set) /// null. /// public StringSegment GetParameter(string parameterName) - { - return GetParameter(new StringSegment(parameterName)); - } + => _mediaTypeHeaderValue.GetParameter(parameterName); /// /// Gets the parameter of the media type. @@ -312,19 +182,7 @@ public StringSegment GetParameter(string parameterName) /// null. /// public StringSegment GetParameter(StringSegment parameterName) - { - var parametersParser = _parameterParser; - - while (parametersParser.ParseNextParameter(out var parameter)) - { - if (parameter.HasName(parameterName)) - { - return parameter.Value; - } - } - - return new StringSegment(); - } + => _mediaTypeHeaderValue.GetParameter(parameterName); /// /// Replaces the encoding of the given with the provided @@ -404,7 +262,7 @@ public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding) /// The parsed media type with its associated quality. public static MediaTypeSegmentWithQuality CreateMediaTypeSegmentWithQuality(string mediaType, int start) { - var parsedMediaType = new MediaType(mediaType, start, length: null); + var parsedMediaType = new ReadOnlyMediaTypeHeaderValue(mediaType, start, length: null); // Short-circuit use of the MediaTypeParameterParser if constructor detected an invalid type or subtype. // Parser would set ParsingFailed==true in this case. But, we handle invalid parameters as a separate case. @@ -414,16 +272,16 @@ public static MediaTypeSegmentWithQuality CreateMediaTypeSegmentWithQuality(stri return default(MediaTypeSegmentWithQuality); } - var parser = parsedMediaType._parameterParser; - var quality = 1.0d; + + var parser = parsedMediaType.ParameterParser; while (parser.ParseNextParameter(out var parameter)) { if (parameter.HasName(QualityParameter)) { // If media type contains two `q` values i.e. it's invalid in an uncommon way, pick last value. quality = double.Parse( - parameter.Value.Value, NumberStyles.AllowDecimalPoint, + parameter.Value.AsSpan(), NumberStyles.AllowDecimalPoint, NumberFormatInfo.InvariantInfo); } } @@ -542,7 +400,7 @@ private bool ContainsAllParameters(MediaTypeParameterParser setParameters) // Copy the parser as we need to iterate multiple times over it. // We can do this because it's a struct - var subSetParameters = _parameterParser; + var subSetParameters = _mediaTypeHeaderValue.ParameterParser; parameterFound = false; while (subSetParameters.ParseNextParameter(out var subSetParameter) && !parameterFound) { diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index b2134e5d98d9..d34e969bd18d 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core MVC core components. Contains common action result types, attribute routing, application model conventions, API explorer, application parts, filters, formatters, model binding, and more. @@ -31,6 +31,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + + diff --git a/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs index f77902bf0ee3..ca2ae7a27c12 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs @@ -21,8 +21,11 @@ public class SimpleWithWebApplicationBuilderTests : IClassFixture fixture) { _fixture = fixture; + Client = _fixture.CreateDefaultClient(); } + public HttpClient Client { get; } + [Fact] public async Task HelloWorld() { @@ -187,5 +190,45 @@ public async Task Environment_Can_Be_Overridden() // Assert Assert.Equal(expected, content); } + + [Fact] + public async Task Accepts_Json_WhenBindingAComplexType() + { + // Act + var response = await Client.PostAsJsonAsync("accepts-default", new { name = "Test" }); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task Rejects_NonJson_WhenBindingAComplexType() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Post, "accepts-default"); + request.Content = new StringContent(""); + request.Content.Headers.ContentType = new("application/xml"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); + } + + [Fact] + public async Task Accepts_NonJsonMediaType() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Post, "accepts-xml"); + request.Content = new StringContent(""); + request.Content.Headers.ContentType = new("application/xml"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.Accepted); + } } } diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs index 213aec9278d2..35805633ed7c 100644 --- a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs @@ -37,6 +37,9 @@ app.MapGet("/greeting", (IConfiguration config) => config["Greeting"]); +app.MapPost("/accepts-default", (Person person) => Results.Ok(person.Name)); +app.MapPost("/accepts-xml", () => Accepted()).Accepts("application/xml"); + app.Run(); record Person(string Name, int Age); @@ -45,4 +48,4 @@ public class MyController : ControllerBase { [HttpGet("/greet")] public string Greet() => $"Hello human"; -} \ No newline at end of file +} diff --git a/src/Shared/MediaType/HttpTokenParsingRule.cs b/src/Shared/MediaType/HttpTokenParsingRule.cs new file mode 100644 index 000000000000..0910a3a20349 --- /dev/null +++ b/src/Shared/MediaType/HttpTokenParsingRule.cs @@ -0,0 +1,277 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.Http.Headers; + +internal static class HttpTokenParsingRules +{ + private static readonly bool[] TokenChars = CreateTokenChars(); + private const int MaxNestedCount = 5; + + internal const char CR = '\r'; + internal const char LF = '\n'; + internal const char SP = ' '; + internal const char Tab = '\t'; + internal const int MaxInt64Digits = 19; + internal const int MaxInt32Digits = 10; + + // iso-8859-1, Western European (ISO) + internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591); + + private static bool[] CreateTokenChars() + { + // token = 1* + // CTL = + + var tokenChars = new bool[128]; // everything is false + + for (var i = 33; i < 127; i++) // skip Space (32) & DEL (127) + { + tokenChars[i] = true; + } + + // remove separators: these are not valid token characters + tokenChars[(byte)'('] = false; + tokenChars[(byte)')'] = false; + tokenChars[(byte)'<'] = false; + tokenChars[(byte)'>'] = false; + tokenChars[(byte)'@'] = false; + tokenChars[(byte)','] = false; + tokenChars[(byte)';'] = false; + tokenChars[(byte)':'] = false; + tokenChars[(byte)'\\'] = false; + tokenChars[(byte)'"'] = false; + tokenChars[(byte)'/'] = false; + tokenChars[(byte)'['] = false; + tokenChars[(byte)']'] = false; + tokenChars[(byte)'?'] = false; + tokenChars[(byte)'='] = false; + tokenChars[(byte)'{'] = false; + tokenChars[(byte)'}'] = false; + + return tokenChars; + } + + internal static bool IsTokenChar(char character) + { + // Must be between 'space' (32) and 'DEL' (127) + if (character > 127) + { + return false; + } + + return TokenChars[character]; + } + + internal static int GetTokenLength(string input, int startIndex) + { + Debug.Assert(input != null); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + while (current < input.Length) + { + if (!IsTokenChar(input[current])) + { + return current - startIndex; + } + current++; + } + return input.Length - startIndex; + } + + internal static int GetWhitespaceLength(string input, int startIndex) + { + Debug.Assert(input != null); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + while (current < input.Length) + { + var c = input[current]; + + if ((c == SP) || (c == Tab)) + { + current++; + continue; + } + + if (c == CR) + { + // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. + if ((current + 2 < input.Length) && (input[current + 1] == LF)) + { + var spaceOrTab = input[current + 2]; + if ((spaceOrTab == SP) || (spaceOrTab == Tab)) + { + current += 3; + continue; + } + } + } + + return current - startIndex; + } + + // All characters between startIndex and the end of the string are LWS characters. + return input.Length - startIndex; + } + + internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) + { + var nestedCount = 0; + return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); + } + + // quoted-pair = "\" CHAR + // CHAR = + internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) + { + Debug.Assert(input != null); + Debug.Assert((startIndex >= 0) && (startIndex < input.Length)); + + length = 0; + + if (input[startIndex] != '\\') + { + return HttpParseResult.NotParsed; + } + + // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) + // If so, check whether the character is in the range 0-127. If not, it's an invalid value. + if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) + { + return HttpParseResult.InvalidFormat; + } + + // We don't care what the char next to '\' is. + length = 2; + return HttpParseResult.Parsed; + } + + // TEXT = + // LWS = [CRLF] 1*( SP | HT ) + // CTL = + // + // Since we don't really care about the content of a quoted string or comment, we're more tolerant and + // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). + // + // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like + // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested + // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) + // is unusual. + private static HttpParseResult GetExpressionLength( + string input, + int startIndex, + char openChar, + char closeChar, + bool supportsNesting, + ref int nestedCount, + out int length) + { + Debug.Assert(input != null); + Debug.Assert((startIndex >= 0) && (startIndex < input.Length)); + + length = 0; + + if (input[startIndex] != openChar) + { + return HttpParseResult.NotParsed; + } + + var current = startIndex + 1; // Start parsing with the character next to the first open-char + while (current < input.Length) + { + // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. + // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. + if ((current + 2 < input.Length) && + (GetQuotedPairLength(input, current, out var quotedPairLength) == HttpParseResult.Parsed)) + { + // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, + // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only + // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). + current = current + quotedPairLength; + continue; + } + + // If we support nested expressions and we find an open-char, then parse the nested expressions. + if (supportsNesting && (input[current] == openChar)) + { + nestedCount++; + try + { + // Check if we exceeded the number of nested calls. + if (nestedCount > MaxNestedCount) + { + return HttpParseResult.InvalidFormat; + } + + var nestedResult = GetExpressionLength( + input, + current, + openChar, + closeChar, + supportsNesting, + ref nestedCount, + out var nestedLength); + + switch (nestedResult) + { + case HttpParseResult.Parsed: + current += nestedLength; // add the length of the nested expression and continue. + break; + + case HttpParseResult.NotParsed: + Debug.Fail("'NotParsed' is unexpected: We started nested expression " + + "parsing, because we found the open-char. So either it's a valid nested " + + "expression or it has invalid format."); + break; + + case HttpParseResult.InvalidFormat: + // If the nested expression is invalid, we can't continue, so we fail with invalid format. + return HttpParseResult.InvalidFormat; + + default: + Debug.Fail("Unknown enum result: " + nestedResult); + break; + } + } + finally + { + nestedCount--; + } + } + + if (input[current] == closeChar) + { + length = current - startIndex + 1; + return HttpParseResult.Parsed; + } + current++; + } + + // We didn't see the final quote, therefore we have an invalid expression string. + return HttpParseResult.InvalidFormat; + } +} + +internal enum HttpParseResult +{ + Parsed, + NotParsed, + InvalidFormat, +} + diff --git a/src/Shared/MediaType/ReadOnlyMediaTypeHeaderValue.cs b/src/Shared/MediaType/ReadOnlyMediaTypeHeaderValue.cs new file mode 100644 index 000000000000..7eaa8903334e --- /dev/null +++ b/src/Shared/MediaType/ReadOnlyMediaTypeHeaderValue.cs @@ -0,0 +1,625 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Headers; + +/// +/// A media type value. +/// +internal readonly struct ReadOnlyMediaTypeHeaderValue +{ + /// + /// Initializes a instance. + /// + /// The with the media type. + public ReadOnlyMediaTypeHeaderValue(string mediaType) + : this(mediaType, 0, mediaType.Length) + { + } + + /// + /// Initializes a instance. + /// + /// The with the media type. + public ReadOnlyMediaTypeHeaderValue(StringSegment mediaType) + : this(mediaType.Buffer, mediaType.Offset, mediaType.Length) + { + } + + /// + /// Initializes a instance. + /// + /// The with the media type. + /// The offset in the where the parsing starts. + /// The length of the media type to parse if provided. + public ReadOnlyMediaTypeHeaderValue(string mediaType, int offset, int? length) + { + ParameterParser = default(MediaTypeParameterParser); + + var typeLength = GetTypeLength(mediaType, offset, out var type); + if (typeLength == 0) + { + Type = new StringSegment(); + SubType = new StringSegment(); + SubTypeWithoutSuffix = new StringSegment(); + SubTypeSuffix = new StringSegment(); + return; + } + else + { + Type = type; + } + + var subTypeLength = GetSubtypeLength(mediaType, offset + typeLength, out var subType); + if (subTypeLength == 0) + { + SubType = new StringSegment(); + SubTypeWithoutSuffix = new StringSegment(); + SubTypeSuffix = new StringSegment(); + return; + } + else + { + SubType = subType; + + if (TryGetSuffixLength(subType, out var subtypeSuffixLength)) + { + SubTypeWithoutSuffix = subType.Subsegment(0, subType.Length - subtypeSuffixLength - 1); + SubTypeSuffix = subType.Subsegment(subType.Length - subtypeSuffixLength, subtypeSuffixLength); + } + else + { + SubTypeWithoutSuffix = SubType; + SubTypeSuffix = new StringSegment(); + } + } + + ParameterParser = new MediaTypeParameterParser(mediaType, offset + typeLength + subTypeLength, length); + } + + // All GetXXXLength methods work in the same way. They expect to be on the right position for + // the token they are parsing, for example, the beginning of the media type or the delimiter + // from a previous token, like '/', ';' or '='. + // Each method consumes the delimiter token if any, the leading whitespace, then the given token + // itself, and finally the trailing whitespace. + private static int GetTypeLength(string input, int offset, out StringSegment type) + { + if (offset < 0 || offset >= input.Length) + { + type = default(StringSegment); + return 0; + } + + var current = offset + HttpTokenParsingRules.GetWhitespaceLength(input, offset); + + // Parse the type, i.e. in media type string "/; param1=value1; param2=value2" + var typeLength = HttpTokenParsingRules.GetTokenLength(input, current); + if (typeLength == 0) + { + type = default(StringSegment); + return 0; + } + + type = new StringSegment(input, current, typeLength); + + current += typeLength; + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - offset; + } + + private static int GetSubtypeLength(string input, int offset, out StringSegment subType) + { + var current = offset; + + // Parse the separator between type and subtype + if (current < 0 || current >= input.Length || input[current] != '/') + { + subType = default(StringSegment); + return 0; + } + + current++; // skip delimiter. + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var subtypeLength = HttpTokenParsingRules.GetTokenLength(input, current); + if (subtypeLength == 0) + { + subType = default(StringSegment); + return 0; + } + + subType = new StringSegment(input, current, subtypeLength); + + current += subtypeLength; + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - offset; + } + + private static bool TryGetSuffixLength(StringSegment subType, out int suffixLength) + { + // Find the last instance of '+', if there is one + var startPos = subType.Offset + subType.Length - 1; + for (var currentPos = startPos; currentPos >= subType.Offset; currentPos--) + { + if (subType.Buffer[currentPos] == '+') + { + suffixLength = startPos - currentPos; + return true; + } + } + + suffixLength = 0; + return false; + } + + /// + /// Gets the type of the . + /// + /// + /// For the media type "application/json", this property gives the value "application". + /// + public StringSegment Type { get; } + + /// + /// Gets whether this matches all types. + /// + public bool MatchesAllTypes => Type.Equals("*", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the subtype of the . + /// + /// + /// For the media type "application/vnd.example+json", this property gives the value + /// "vnd.example+json". + /// + public StringSegment SubType { get; } + + /// + /// Gets the subtype of the , excluding any structured syntax suffix. + /// + /// + /// For the media type "application/vnd.example+json", this property gives the value + /// "vnd.example". + /// + public StringSegment SubTypeWithoutSuffix { get; } + + /// + /// Gets the structured syntax suffix of the if it has one. + /// + /// + /// For the media type "application/vnd.example+json", this property gives the value + /// "json". + /// + public StringSegment SubTypeSuffix { get; } + + /// + /// Gets whether this matches all subtypes. + /// + /// + /// For the media type "application/*", this property is true. + /// + /// + /// For the media type "application/json", this property is false. + /// + public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets whether this matches all subtypes, ignoring any structured syntax suffix. + /// + /// + /// For the media type "application/*+json", this property is true. + /// + /// + /// For the media type "application/vnd.example+json", this property is false. + /// + public bool MatchesAllSubTypesWithoutSuffix => SubTypeWithoutSuffix.Equals("*", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the of the if it has one. + /// + public Encoding? Encoding => GetEncodingFromCharset(GetParameter("charset")); + + /// + /// Gets the charset parameter of the if it has one. + /// + public StringSegment Charset => GetParameter("charset"); + + /// + /// Determines whether the current contains a wildcard. + /// + /// + /// true if this contains a wildcard; otherwise false. + /// + public bool HasWildcard + { + get + { + return MatchesAllTypes || + MatchesAllSubTypesWithoutSuffix || + GetParameter("*").Equals("*", StringComparison.OrdinalIgnoreCase); + } + } + + public MediaTypeParameterParser ParameterParser { get; } + + /// + /// Determines whether the current is a subset of the + /// . + /// + /// The set . + /// + /// true if this is a subset of ; otherwise false. + /// + public bool IsSubsetOf(ReadOnlyMediaTypeHeaderValue set) + { + return MatchesType(set) && + MatchesSubtype(set) && + ContainsAllParameters(set.ParameterParser); + } + + /// + /// Gets the parameter of the media type. + /// + /// The name of the parameter to retrieve. + /// + /// The for the given if found; otherwise + /// null. + /// + public StringSegment GetParameter(string parameterName) + { + return GetParameter(new StringSegment(parameterName)); + } + + /// + /// Gets the parameter of the media type. + /// + /// The name of the parameter to retrieve. + /// + /// The for the given if found; otherwise + /// null. + /// + public StringSegment GetParameter(StringSegment parameterName) + { + var parametersParser = ParameterParser; + + while (parametersParser.ParseNextParameter(out var parameter)) + { + if (parameter.HasName(parameterName)) + { + return parameter.Value; + } + } + + return new StringSegment(); + } + + /// + /// Gets the last parameter of the media type. + /// + /// The name of the parameter to retrieve. + /// The value for the last parameter + /// + /// if parsing succeeded. + /// + public bool TryGetLastParameter(StringSegment parameterName, out StringSegment parameterValue) + { + var parametersParser = ParameterParser; + + parameterValue = default; + while (parametersParser.ParseNextParameter(out var parameter)) + { + if (parameter.HasName(parameterName)) + { + parameterValue = parameter.Value; + } + } + + return !parametersParser.ParsingFailed; + } + + /// + /// Get an encoding for a mediaType. + /// + /// The mediaType. + /// The encoding. + public static Encoding? GetEncoding(string mediaType) + { + return GetEncoding(new StringSegment(mediaType)); + } + + /// + /// Get an encoding for a mediaType. + /// + /// The mediaType. + /// The encoding. + public static Encoding? GetEncoding(StringSegment mediaType) + { + var parsedMediaType = new ReadOnlyMediaTypeHeaderValue(mediaType); + return parsedMediaType.Encoding; + } + + private static Encoding? GetEncodingFromCharset(StringSegment charset) + { + if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) + { + // This is an optimization for utf-8 that prevents the Substring caused by + // charset.Value + return Encoding.UTF8; + } + + try + { + // charset.Value might be an invalid encoding name as in charset=invalid. + // For that reason, we catch the exception thrown by Encoding.GetEncoding + // and return null instead. + return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null; + } + catch (Exception) + { + return null; + } + } + + private static string CreateMediaTypeWithEncoding(StringSegment mediaType, Encoding encoding) + { + return $"{mediaType.Value}; charset={encoding.WebName}"; + } + + private bool MatchesType(ReadOnlyMediaTypeHeaderValue set) + { + return set.MatchesAllTypes || + set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesSubtype(ReadOnlyMediaTypeHeaderValue set) + { + if (set.MatchesAllSubTypes) + { + return true; + } + + if (set.SubTypeSuffix.HasValue) + { + if (SubTypeSuffix.HasValue) + { + // Both the set and the media type being checked have suffixes, so both parts must match. + return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set); + } + else + { + // The set has a suffix, but the media type being checked doesn't. We never consider this to match. + return false; + } + } + else + { + // If this subtype or suffix matches the subtype of the set, + // it is considered a subtype. + // Ex: application/json > application/val+json + return MatchesEitherSubtypeOrSuffix(set); + } + } + + private bool MatchesSubtypeWithoutSuffix(ReadOnlyMediaTypeHeaderValue set) + { + return set.MatchesAllSubTypesWithoutSuffix || + set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesSubtypeSuffix(ReadOnlyMediaTypeHeaderValue set) + { + // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") + // because there's no clear use case for it. + return set.SubTypeSuffix.Equals(SubTypeSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesEitherSubtypeOrSuffix(ReadOnlyMediaTypeHeaderValue set) + { + return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || + set.SubType.Equals(SubTypeSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool ContainsAllParameters(MediaTypeParameterParser setParameters) + { + var parameterFound = true; + while (setParameters.ParseNextParameter(out var setParameter) && parameterFound) + { + if (setParameter.HasName("q")) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + if (setParameter.HasName("*")) + { + // A parameter named "*" has no effect on media type matching, as it is only used as an indication + // that the entire media type string should be treated as a wildcard. + continue; + } + + // Copy the parser as we need to iterate multiple times over it. + // We can do this because it's a struct + var subSetParameters = ParameterParser; + parameterFound = false; + while (subSetParameters.ParseNextParameter(out var subSetParameter) && !parameterFound) + { + parameterFound = subSetParameter.Equals(setParameter); + } + } + + return parameterFound; + } + + public struct MediaTypeParameterParser + { + private readonly string _mediaTypeBuffer; + private readonly int? _length; + + public MediaTypeParameterParser(string mediaTypeBuffer, int offset, int? length) + { + _mediaTypeBuffer = mediaTypeBuffer; + _length = length; + CurrentOffset = offset; + ParsingFailed = false; + } + + public int CurrentOffset { get; private set; } + + public bool ParsingFailed { get; private set; } + + public bool ParseNextParameter(out MediaTypeParameter result) + { + if (_mediaTypeBuffer == null) + { + ParsingFailed = true; + result = default(MediaTypeParameter); + return false; + } + + var parameterLength = GetParameterLength(_mediaTypeBuffer, CurrentOffset, out result); + CurrentOffset += parameterLength; + + if (parameterLength == 0) + { + ParsingFailed = _length != null && CurrentOffset < _length; + return false; + } + + return true; + } + + private static int GetParameterLength(string input, int startIndex, out MediaTypeParameter parsedValue) + { + if (OffsetIsOutOfRange(startIndex, input.Length) || input[startIndex] != ';') + { + parsedValue = default(MediaTypeParameter); + return 0; + } + + var nameLength = GetNameLength(input, startIndex, out var name); + + var current = startIndex + nameLength; + + if (nameLength == 0 || OffsetIsOutOfRange(current, input.Length) || input[current] != '=') + { + if (current == input.Length && name.Equals("*", StringComparison.OrdinalIgnoreCase)) + { + // As a special case, we allow a trailing ";*" to indicate a wildcard + // string allowing any other parameters. It's the same as ";*=*". + var asterisk = new StringSegment("*"); + parsedValue = new MediaTypeParameter(asterisk, asterisk); + return current - startIndex; + } + else + { + parsedValue = default(MediaTypeParameter); + return 0; + } + } + + var valueLength = GetValueLength(input, current, out var value); + + parsedValue = new MediaTypeParameter(name, value); + current += valueLength; + + return current - startIndex; + } + + private static int GetNameLength(string input, int startIndex, out StringSegment name) + { + var current = startIndex; + + current++; // skip ';' + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var nameLength = HttpTokenParsingRules.GetTokenLength(input, current); + if (nameLength == 0) + { + name = default(StringSegment); + return 0; + } + + name = new StringSegment(input, current, nameLength); + + current += nameLength; + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - startIndex; + } + + private static int GetValueLength(string input, int startIndex, out StringSegment value) + { + var current = startIndex; + + current++; // skip '='. + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var valueLength = HttpTokenParsingRules.GetTokenLength(input, current); + + if (valueLength == 0) + { + // A value can either be a token or a quoted string. Check if it is a quoted string. + var result = HttpTokenParsingRules.GetQuotedStringLength(input, current, out valueLength); + if (result != HttpParseResult.Parsed) + { + // We have an invalid value. Reset the name and return. + value = default(StringSegment); + return 0; + } + + // Quotation marks are not part of a quoted parameter value. + value = new StringSegment(input, current + 1, valueLength - 2); + } + else + { + value = new StringSegment(input, current, valueLength); + } + + current += valueLength; + current += HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - startIndex; + } + + private static bool OffsetIsOutOfRange(int offset, int length) + { + return offset < 0 || offset >= length; + } + } + + public readonly struct MediaTypeParameter : IEquatable + { + public MediaTypeParameter(StringSegment name, StringSegment value) + { + Name = name; + Value = value; + } + + public StringSegment Name { get; } + + public StringSegment Value { get; } + + public bool HasName(string name) + { + return HasName(new StringSegment(name)); + } + + public bool HasName(StringSegment name) + { + return Name.Equals(name, StringComparison.OrdinalIgnoreCase); + } + + public bool Equals(MediaTypeParameter other) + { + return HasName(other.Name) && Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase); + } + + public override string ToString() => $"{Name}={Value}"; + } +} From d4e85d5bbccc15f32b558651191ce41f37ae084d Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Mon, 16 Aug 2021 13:41:24 -0700 Subject: [PATCH 15/29] fix merge errors --- src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index b15e6566295b..ee686e17749b 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.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.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Metadata; From 3de78c69467d4aa7766b7897f9d1346339ca4d83 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Mon, 16 Aug 2021 13:49:35 -0700 Subject: [PATCH 16/29] address pr comment --- .../src/DefaultApiDescriptionProvider.cs | 16 ---------------- .../EndpointMetadataApiDescriptionProvider.cs | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index a3bb20805cc9..5a5d82e1ff6c 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -446,22 +446,6 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList GetAcceptsContentTypes(IReadOnlyList? requestMetadataAttributes) - { - // Walk through all 'filter' attributes in order, and allow each one to see or override - // the results of the previous ones. This is similar to the execution path for content-negotiation. - var contentTypes = new List(); - if (requestMetadataAttributes != null) - { - foreach (var metadataAttribute in requestMetadataAttributes) - { - contentTypes.AddRange(metadataAttribute.ContentTypes); - } - } - - return contentTypes; - } - private static IApiRequestMetadataProvider[]? GetRequestMetadataAttributes(ControllerActionDescriptor action) { if (action.FilterDescriptors == null) diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 405371f10bec..9adef9d0c06f 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -239,7 +239,7 @@ private static void AddAcceptsRequestFormats( EndpointMetadataCollection endpointMetadata) { var requestMetadata = endpointMetadata.GetOrderedMetadata(); - var declaredContentTypes = DefaultApiDescriptionProvider.GetAcceptsContentTypes(requestMetadata); + var declaredContentTypes = GetAcceptsContentTypes(requestMetadata); if (declaredContentTypes.Count > 0) { @@ -372,6 +372,20 @@ private static void AddResponseContentTypes(IList apiResponse } } + private static IReadOnlyList GetAcceptsContentTypes(IReadOnlyList? requestMetadataAttributes) + { + var contentTypes = new List(); + if (requestMetadataAttributes != null) + { + foreach (var metadataAttribute in requestMetadataAttributes) + { + contentTypes.AddRange(metadataAttribute.ContentTypes); + } + } + + return contentTypes; + } + private static void AddActionDescriptorEndpointMetadata( ActionDescriptor actionDescriptor, EndpointMetadataCollection endpointMetadata) From c3d2cd307b4b55ecd085e28a9d806abe6be3eb09 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Mon, 16 Aug 2021 14:53:03 -0700 Subject: [PATCH 17/29] fix test error --- .../Http.Extensions/test/RequestDelegateFactoryTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index d7cd84c3693f..232c30c3774a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -260,7 +260,7 @@ public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { if (id is not null) { @@ -278,6 +278,8 @@ public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() ["id"] = "42" }; + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); Assert.Equal(41, httpContext.Items["input"]); @@ -288,7 +290,7 @@ public async Task NullRouteParametersPrefersRouteOverQueryString() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { if (id is not null) { @@ -306,6 +308,7 @@ public async Task NullRouteParametersPrefersRouteOverQueryString() ["id"] = "42" }; + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(42, httpContext.Items["input"]); From 9ec2bcd9b9cd7fe51abb98982ee01cf662f488ac Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Mon, 16 Aug 2021 15:01:37 -0700 Subject: [PATCH 18/29] remove options from params --- .../Http.Extensions/src/RequestDelegateFactory.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index e2c862a592aa..269568179714 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; -using static System.Collections.Specialized.BitVector32; namespace Microsoft.AspNetCore.Http { @@ -161,7 +160,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func 0 ? CreateParamCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments, factoryContext) : @@ -175,7 +174,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func().FirstOrDefault() is { } bodyAttribute) { - return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext, options); + return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext); } else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) { @@ -278,7 +277,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext } } - return BindParameterFromBody(parameter, allowEmpty: false, factoryContext, options); + return BindParameterFromBody(parameter, allowEmpty: false, factoryContext); } } @@ -721,7 +720,7 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext); } - private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext, RequestDelegateFactoryOptions? options) + private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) { if (factoryContext.JsonRequestBodyType is not null) { From 9d440abd636585881780656dbe0871cede200f8e Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Wed, 18 Aug 2021 16:20:51 -0700 Subject: [PATCH 19/29] implements iacceptsMetadata --- .../src/Metadata/IAcceptsMetadata.cs | 4 +- .../src/PublicAPI.Unshipped.txt | 2 - .../src/RequestDelegateResult.cs | 5 +- .../IApiRequestMetadataProvider.cs | 1 + ...nApiEndpointConventionBuilderExtensions.cs | 47 ++----------------- src/Mvc/Mvc.Core/src/ConsumesAttribute.cs | 47 ++++++++++++++++++- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 6 +-- 7 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index cba34691998b..2e4ff40b66e0 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -12,12 +12,12 @@ namespace Microsoft.AspNetCore.Http.Metadata public interface IAcceptsMetadata { /// - /// Gets a list of request content types. + /// Gets a list of the allowed request content types. If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response. /// IReadOnlyList ContentTypes { get; } /// - /// Accepts request content types of any shape. + /// Gets the type being read from the request. /// Type? RequestType { get; } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 08b5060e4403..639b5cec95fc 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -28,9 +28,7 @@ Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDel Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? Microsoft.AspNetCore.Http.RequestDelegateResult Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate! -Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.init -> void Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, System.Collections.Generic.IReadOnlyList! metadata) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string! diff --git a/src/Http/Http.Abstractions/src/RequestDelegateResult.cs b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs index 31afc40d6ce7..88ddd28a4173 100644 --- a/src/Http/Http.Abstractions/src/RequestDelegateResult.cs +++ b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs @@ -22,13 +22,12 @@ public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList /// Gets the /// - /// A task that represents the completion of request processing. - public RequestDelegate RequestDelegate { get; init; } + public RequestDelegate RequestDelegate { get;} /// /// Gets endpoint metadata inferred from creating the /// - public IReadOnlyList EndpointMetadata { get; init; } + public IReadOnlyList EndpointMetadata { get;} } } diff --git a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs index bb096cddd408..45b7b1ffe9c7 100644 --- a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiRequestMetadataProvider.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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 5aa2b107f2c2..9f79f599f085 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -45,7 +45,7 @@ public static MinimalActionEndpointConventionBuilder ExcludeFromDescription(this public static MinimalActionEndpointConventionBuilder Produces(this MinimalActionEndpointConventionBuilder builder, #pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, - string? contentType = null, + string? contentType = null, params string[] additionalContentTypes) { return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); @@ -125,7 +125,6 @@ public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(t return Produces(builder, statusCode, contentType); } -#pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Adds the to for all builders /// produced by . @@ -135,17 +134,13 @@ public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(t /// The request content type. Defaults to "application/json" if empty. /// Additional response content types the endpoint produces for the supplied status code. /// A that can be used to further customize the endpoint. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, string? contentType = null, params string[] additionalContentTypes) -#pragma warning restore CS0419 // Ambiguous reference in cref attribute -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, string contentType, params string[] additionalContentTypes) { Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); return builder; } -#pragma warning disable CS0419 // Ambiguous reference in cref attribute /// /// Adds the to for all builders /// produced by . @@ -155,48 +150,16 @@ public static MinimalActionEndpointConventionBuilder Accepts(this Mini /// The response content type that the endpoint accepts. /// Additional response content types the endpoint accepts /// A that can be used to further customize the endpoint. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, Type requestType , -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters -#pragma warning restore CS0419 // Ambiguous reference in cref attribute - string? contentType = null, params string[] additionalContentTypes) - { - builder.WithMetadata(new AcceptsMetadata(requestType, GetAllContentTypes(contentType, additionalContentTypes))); - return builder; - } - -#pragma warning disable CS0419 // Ambiguous reference in cref attribute - /// - /// Adds the to for all builders - /// produced by . - /// - /// The . - /// The response content type that the endpoint accepts. - /// Additional response content types the endpoint accepts - /// A that can be used to further customize the endpoint. - public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, -#pragma warning restore CS0419 // Ambiguous reference in cref attribute + public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, Type requestType, string contentType, params string[] additionalContentTypes) - { - var allContentTypes = GetAllContentTypes(contentType, additionalContentTypes); - builder.WithMetadata(new AcceptsMetadata(allContentTypes)); - - return builder; - } - - private static string[] GetAllContentTypes(string? contentType, string[] additionalContentTypes) { if (string.IsNullOrEmpty(contentType)) { contentType = "application/json"; } - var allContentTypes = new List() - { - contentType - }; - allContentTypes.AddRange(additionalContentTypes); - return allContentTypes.ToArray(); + builder.WithMetadata(new ConsumesAttribute(requestType, contentType, additionalContentTypes)); + return builder; } } } diff --git a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs index ea4e3a6ae635..f2d674848cc8 100644 --- a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -23,7 +24,8 @@ public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint, - IApiRequestMetadataProvider + IApiRequestMetadataProvider, + IAcceptsMetadata { /// /// The order for consumes attribute. @@ -53,6 +55,31 @@ public ConsumesAttribute(string contentType, params string[] otherContentTypes) ContentTypes = GetContentTypes(contentType, otherContentTypes); } + /// + /// Creates a new instance of . + /// + public ConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes) + { + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + // We want to ensure that the given provided content types are valid values, so + // we validate them using the semantics of MediaTypeHeaderValue. + MediaTypeHeaderValue.Parse(contentType); + + for (var i = 0; i < otherContentTypes.Length; i++) + { + MediaTypeHeaderValue.Parse(otherContentTypes[i]); + } + + ContentTypes = GetContentTypes(contentType, otherContentTypes); + _contentTypes = GetAllContentTypes(contentType, otherContentTypes); + _requestType = requestType; + + } + // The value used is a non default value so that it avoids getting mixed with other action constraints // with default order. /// @@ -64,6 +91,14 @@ public ConsumesAttribute(string contentType, params string[] otherContentTypes) /// public MediaTypeCollection ContentTypes { get; set; } + internal readonly Type? _requestType; + + internal readonly List _contentTypes = new(); + + Type? IAcceptsMetadata.RequestType => _requestType; + + IReadOnlyList IAcceptsMetadata.ContentTypes => _contentTypes; + /// public void OnResourceExecuting(ResourceExecutingContext context) { @@ -223,6 +258,16 @@ private MediaTypeCollection GetContentTypes(string firstArg, string[] args) return contentTypes; } + private static List GetAllContentTypes(string contentType, string[] additionalContentTypes) + { + var allContentTypes = new List() + { + contentType + }; + allContentTypes.AddRange(additionalContentTypes); + return allContentTypes; + } + /// public void SetContentTypes(MediaTypeCollection contentTypes) { diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 72d49d876200..0c8597b3f152 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -528,6 +528,7 @@ *REMOVED*~virtual Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase.Content(string contentPath) -> string *REMOVED*~virtual Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase.IsLocalUrl(string url) -> bool *REMOVED*~virtual Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase.Link(string routeName, object values) -> string +Microsoft.AspNetCore.Mvc.ConsumesAttribute.ConsumesAttribute(System.Type! requestType, string! contentType, params string![]! otherContentTypes) -> void Microsoft.AspNetCore.Mvc.JsonOptions.AllowInputFormatterExceptionMessages.get -> bool Microsoft.AspNetCore.Mvc.JsonOptions.AllowInputFormatterExceptionMessages.set -> void Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.CreateAsyncReleaser(Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor! descriptor) -> System.Func? @@ -544,9 +545,8 @@ Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection.Items.get -> Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(string? message) -> void Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor.ContentResultExecutor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory! httpResponseStreamWriterFactory) -> void -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, System.Type! requestType, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, System.Type! requestType, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! ~Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector.DefaultOutputFormatterSelector(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileContentResultExecutor.FileContentResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.FileResultExecutorBase(Microsoft.Extensions.Logging.ILogger! logger) -> void From be64e8e9eea3dd7e696f7d366ef632e25277f07a Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Wed, 18 Aug 2021 23:32:28 -0700 Subject: [PATCH 20/29] fix test failures --- .../test/RequestDelegateFactoryTests.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index c62f25586b41..f71e7f145f8a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -632,11 +632,13 @@ public async Task RequestDelegatePrefersTryParseHttpContextOverTryParseString() httpContext.Request.Headers.Referer = "https://example.org"; - var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextRecord tryParsable) => + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextRecord tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); + var requestDelegate = resultFactory.RequestDelegate; + await requestDelegate(httpContext); Assert.Equal(new MyTryParseHttpContextRecord(new Uri("https://example.org")), httpContext.Items["tryParsable"]); @@ -649,11 +651,12 @@ public async Task RequestDelegatePrefersTryParseHttpContextOverTryParseStringFor httpContext.Request.Headers.Referer = "https://example.org"; - var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextStruct tryParsable) => + var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextStruct tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(new MyTryParseHttpContextStruct(new Uri("https://example.org")), httpContext.Items["tryParsable"]); @@ -662,8 +665,9 @@ public async Task RequestDelegatePrefersTryParseHttpContextOverTryParseStringFor [Fact] public async Task RequestDelegateUsesTryParseStringoOverTryParseHttpContextGivenExplicitAttribute() { - var fromRouteRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyTryParseHttpContextRecord tryParsable) => { }); - var fromQueryRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyTryParseHttpContextRecord tryParsable) => { }); + var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyTryParseHttpContextRecord tryParsable) => { }); + var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyTryParseHttpContextRecord tryParsable) => { }); + var httpContext = new DefaultHttpContext { @@ -680,6 +684,9 @@ public async Task RequestDelegateUsesTryParseStringoOverTryParseHttpContextGiven }, }; + var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; + var fromQueryRequestDelegate = fromQueryFactoryResult.RequestDelegate; + await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); await Assert.ThrowsAsync(() => fromQueryRequestDelegate(httpContext)); } @@ -687,7 +694,7 @@ public async Task RequestDelegateUsesTryParseStringoOverTryParseHttpContextGiven [Fact] public async Task RequestDelegateUsesTryParseStringoOverTryParseHttpContextGivenNullableStruct() { - var fromRouteRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextStruct? tryParsable) => { }); + var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, MyTryParseHttpContextStruct? tryParsable) => { }); var httpContext = new DefaultHttpContext { @@ -700,6 +707,7 @@ public async Task RequestDelegateUsesTryParseStringoOverTryParseHttpContextGiven }, }; + var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); } @@ -789,11 +797,12 @@ public async Task RequestDelegateLogsTryParseHttpContextFailuresAndSets400Respon var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((MyTryParseHttpContextRecord arg1, MyTryParseHttpContextRecord arg2) => + var factoryResult = RequestDelegateFactory.Create((MyTryParseHttpContextRecord arg1, MyTryParseHttpContextRecord arg2) => { invoked = true; }); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.False(invoked); @@ -1887,11 +1896,12 @@ public async Task RequestDelegateDoesNotSupportTryParseHttpContextOptionality() var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((MyTryParseHttpContextRecord? arg1) => + var factoryResult = RequestDelegateFactory.Create((MyTryParseHttpContextRecord? arg1) => { invoked = true; }); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.False(invoked); From 85b38ca16e1439cc90780ca6693864361a2b2b4f Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 19 Aug 2021 09:10:39 -0700 Subject: [PATCH 21/29] fix test failures --- .../test/EndpointMetadataApiDescriptionProviderTest.cs | 5 ++--- .../SimpleWebSiteWithWebApplicationBuilder/Program.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 153ab428f1fa..2eabd6095bf4 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -592,7 +591,7 @@ public void HandleAcceptsMetadata() // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); builder.MapPost("/api/todos", () => "") - .Accepts("application/json", "application/xml"); + .Accepts("application/json", "application/xml"); var context = new ApiDescriptionProviderContext(Array.Empty()); var endpointDataSource = builder.DataSources.OfType().Single(); @@ -625,7 +624,7 @@ public void HandleAcceptsMetadataWhenEmptyReturnsDefaultContentType() // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); builder.MapPost("/api/todos", () => "") - .Accepts(""); + .Accepts(typeof(string), ""); var context = new ApiDescriptionProviderContext(Array.Empty()); var endpointDataSource = builder.DataSources.OfType().Single(); diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs index 35805633ed7c..b0b21f572be3 100644 --- a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs @@ -38,7 +38,7 @@ app.MapGet("/greeting", (IConfiguration config) => config["Greeting"]); app.MapPost("/accepts-default", (Person person) => Results.Ok(person.Name)); -app.MapPost("/accepts-xml", () => Accepted()).Accepts("application/xml"); +app.MapPost("/accepts-xml", () => Accepted()).Accepts("application/xml"); app.Run(); From f6762c99dd621a22765e59a78b73ebf478396c43 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 19 Aug 2021 14:21:25 -0700 Subject: [PATCH 22/29] move iacceptmetadata to shared source --- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 5 ----- .../src/Microsoft.AspNetCore.Http.Extensions.csproj | 3 ++- .../UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj | 6 +++++- .../Metadata => Shared/RoutingMetadata}/AcceptsMetadata.cs | 3 ++- 4 files changed, 9 insertions(+), 8 deletions(-) rename src/{Http/Http.Abstractions/src/Metadata => Shared/RoutingMetadata}/AcceptsMetadata.cs (95%) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 639b5cec95fc..d689bce14e4b 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -7,11 +7,6 @@ *REMOVED*abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string! Microsoft.AspNetCore.Http.IResult Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(System.Type? type, string![]! contentTypes) -> void -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes) -> void -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.RequestType.get -> System.Type? Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type? diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 7e4e66fe03aa..93a83901bac1 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state. @@ -16,6 +16,7 @@ + diff --git a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj index 9d9344c05084..7b6df8ebded4 100644 --- a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj +++ b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -14,4 +14,8 @@ + + + + diff --git a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs b/src/Shared/RoutingMetadata/AcceptsMetadata.cs similarity index 95% rename from src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs rename to src/Shared/RoutingMetadata/AcceptsMetadata.cs index fc1329ceeaed..1d030a80d022 100644 --- a/src/Http/Http.Abstractions/src/Metadata/AcceptsMetadata.cs +++ b/src/Shared/RoutingMetadata/AcceptsMetadata.cs @@ -4,12 +4,13 @@ using System; using System.Collections.Generic; +#nullable enable namespace Microsoft.AspNetCore.Http.Metadata { /// /// Metadata that specifies the supported request content types. /// - public sealed class AcceptsMetadata : IAcceptsMetadata + internal sealed class AcceptsMetadata : IAcceptsMetadata { /// /// Creates a new instance of . From d4daaa7a2a3ecaab53c2a5344155059c8463cf04 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 19 Aug 2021 15:38:28 -0700 Subject: [PATCH 23/29] add acceptsmetadata shared code to mvc --- src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index d34e969bd18d..a0b45ab9d83a 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core MVC core components. Contains common action result types, attribute routing, application model conventions, API explorer, application parts, filters, formatters, model binding, and more. @@ -33,6 +33,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + From f43d3c0b193de56eb4b95a2dedeeb010679abec1 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Thu, 19 Aug 2021 16:16:16 -0700 Subject: [PATCH 24/29] fix tests --- .../test/RequestDelegateFactoryTests.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 6da2fcee2620..6c7b5715b9d8 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -636,7 +636,7 @@ public async Task RequestDelegatePrefersBindAsyncOverTryParseString() httpContext.Request.Headers.Referer = "https://example.org"; - var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncRecord tryParsable) => + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncRecord tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); @@ -655,12 +655,12 @@ public async Task RequestDelegatePrefersBindAsyncOverTryParseStringForNonNullabl httpContext.Request.Headers.Referer = "https://example.org"; - var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct tryParsable) => + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); - var requestDelegate = factoryResult.RequestDelegate; + var requestDelegate = resultFactory.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["tryParsable"]); @@ -669,8 +669,8 @@ public async Task RequestDelegatePrefersBindAsyncOverTryParseStringForNonNullabl [Fact] public async Task RequestDelegateUsesTryParseStringoOverBindAsyncGivenExplicitAttribute() { - var fromRouteRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyBindAsyncRecord tryParsable) => { }); - var fromQueryRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord tryParsable) => { }); + var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyBindAsyncRecord tryParsable) => { }); + var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord tryParsable) => { }); var httpContext = new DefaultHttpContext @@ -698,7 +698,7 @@ public async Task RequestDelegateUsesTryParseStringoOverBindAsyncGivenExplicitAt [Fact] public async Task RequestDelegateUsesTryParseStringOverBindAsyncGivenNullableStruct() { - var fromRouteRequestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct? tryParsable) => { }); + var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct? tryParsable) => { }); var httpContext = new DefaultHttpContext { @@ -801,7 +801,7 @@ public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((MyBindAsyncRecord arg1, MyBindAsyncRecord arg2) => + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord arg1, MyBindAsyncRecord arg2) => { invoked = true; }); @@ -835,7 +835,9 @@ public async Task BindAsyncExceptionsThrowException() RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), }; - var requestDelegate = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { }); + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { }); + + var requestDelegate = factoryResult.RequestDelegate; var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); Assert.Equal("BindAsync failed", ex.Message); @@ -877,13 +879,15 @@ public async Task BindAsyncWithBodyArgument() var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((HttpContext context, MyBindAsyncRecord arg1, Todo todo) => + var factoryResult = RequestDelegateFactory.Create((HttpContext context, MyBindAsyncRecord arg1, Todo todo) => { invoked = true; context.Items[nameof(arg1)] = arg1; context.Items[nameof(todo)] = todo; }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); Assert.True(invoked); @@ -931,13 +935,15 @@ public async Task BindAsyncRunsBeforeBodyBinding() var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((HttpContext context, CustomTodo customTodo, Todo todo) => + var factoryResult = RequestDelegateFactory.Create((HttpContext context, CustomTodo customTodo, Todo todo) => { invoked = true; context.Items[nameof(customTodo)] = customTodo; context.Items[nameof(todo)] = todo; }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); Assert.True(invoked); @@ -2021,7 +2027,7 @@ public async Task RequestDelegateDoesSupportBindAsyncOptionality() var invoked = false; - var requestDelegate = RequestDelegateFactory.Create((MyBindAsyncRecord? arg1) => + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord? arg1) => { invoked = true; }); From 2b638db58464c34a1d3bdfdd40dae1f5d316b54d Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 20 Aug 2021 11:37:05 -0700 Subject: [PATCH 25/29] address pr comments --- .../src/Metadata/IAcceptsMetadata.cs | 3 ++- .../Http.Extensions/src/RequestDelegateFactory.cs | 6 ++++-- .../src/Microsoft.AspNetCore.Routing.csproj | 14 ++++++++------ .../OpenApiEndpointConventionBuilderExtensions.cs | 5 ----- src/Mvc/Mvc.Core/src/ConsumesAttribute.cs | 13 +++++++++---- src/Shared/RoutingMetadata/AcceptsMetadata.cs | 4 ++-- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index 2e4ff40b66e0..a3325158a7ac 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -12,7 +12,8 @@ namespace Microsoft.AspNetCore.Http.Metadata public interface IAcceptsMetadata { /// - /// Gets a list of the allowed request content types. If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response. + /// Gets a list of the allowed request content types. + /// If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response. /// IReadOnlyList ContentTypes { get; } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index d2e6e6e40fb7..2a1a375afdf8 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -64,6 +64,8 @@ public static partial class RequestDelegateFactory private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null)); private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null)); + private static readonly AcceptsMetadata DefaultAcceptsMetadata = new(new[] { "application/json" }); + /// /// Creates a implementation for . /// @@ -140,7 +142,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); } - private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) { // Non void return type @@ -860,7 +862,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al } } - factoryContext.Metadata.Add(new AcceptsMetadata(new string[] { "application/json" })); + factoryContext.Metadata.Add(DefaultAcceptsMetadata); var nullability = NullabilityContext.Create(parameter); var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable; diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 676fe290e560..20e688ff0daf 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -1,10 +1,12 @@ - ASP.NET Core middleware for routing requests to application logic and for generating links. -Commonly used types: -Microsoft.AspNetCore.Routing.Route -Microsoft.AspNetCore.Routing.RouteCollection + + ASP.NET Core middleware for routing requests to application logic and for generating links. + Commonly used types: + Microsoft.AspNetCore.Routing.Route + Microsoft.AspNetCore.Routing.RouteCollection + $(DefaultNetCoreTargetFramework) true true @@ -26,8 +28,8 @@ Microsoft.AspNetCore.Routing.RouteCollection - - + + diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 9f79f599f085..dbb4326f4465 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -153,11 +153,6 @@ public static MinimalActionEndpointConventionBuilder Accepts(this Mini public static MinimalActionEndpointConventionBuilder Accepts(this MinimalActionEndpointConventionBuilder builder, Type requestType, string contentType, params string[] additionalContentTypes) { - if (string.IsNullOrEmpty(contentType)) - { - contentType = "application/json"; - } - builder.WithMetadata(new ConsumesAttribute(requestType, contentType, additionalContentTypes)); return builder; } diff --git a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs index f2d674848cc8..817e54a724d5 100644 --- a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs @@ -35,6 +35,8 @@ public class ConsumesAttribute : /// /// Creates a new instance of . + /// The request content type + /// The additional list of allowed request content types /// public ConsumesAttribute(string contentType, params string[] otherContentTypes) { @@ -57,6 +59,9 @@ public ConsumesAttribute(string contentType, params string[] otherContentTypes) /// /// Creates a new instance of . + /// The type being read from the request + /// The request content type + /// The additional list of allowed request content types /// public ConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes) { @@ -74,10 +79,10 @@ public ConsumesAttribute(Type requestType, string contentType, params string[] o MediaTypeHeaderValue.Parse(otherContentTypes[i]); } - ContentTypes = GetContentTypes(contentType, otherContentTypes); + ContentTypes = GetContentTypes(contentType, otherContentTypes); _contentTypes = GetAllContentTypes(contentType, otherContentTypes); _requestType = requestType; - + } // The value used is a non default value so that it avoids getting mixed with other action constraints @@ -91,9 +96,9 @@ public ConsumesAttribute(Type requestType, string contentType, params string[] o /// public MediaTypeCollection ContentTypes { get; set; } - internal readonly Type? _requestType; + readonly Type? _requestType; - internal readonly List _contentTypes = new(); + readonly List _contentTypes = new(); Type? IAcceptsMetadata.RequestType => _requestType; diff --git a/src/Shared/RoutingMetadata/AcceptsMetadata.cs b/src/Shared/RoutingMetadata/AcceptsMetadata.cs index 1d030a80d022..eadfd5deb565 100644 --- a/src/Shared/RoutingMetadata/AcceptsMetadata.cs +++ b/src/Shared/RoutingMetadata/AcceptsMetadata.cs @@ -1,10 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using System; using System.Collections.Generic; -#nullable enable namespace Microsoft.AspNetCore.Http.Metadata { /// @@ -45,7 +46,6 @@ public AcceptsMetadata(Type? type, string[] contentTypes) /// public IReadOnlyList ContentTypes { get; } - /// /// Accepts request content types of any shape. /// From 14a7ff96a3f10484515482fcc8a3239b17dd3598 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 20 Aug 2021 11:45:57 -0700 Subject: [PATCH 26/29] address another comment --- .../src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 854cad7060c7..1eae726c682d 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -207,7 +207,7 @@ public static MinimalActionEndpointConventionBuilder Map( var attributes = action.Method.GetCustomAttributes(); //Add add request delegate metadata - foreach(var metadata in requestDelegateResult.EndpointMetadata) + foreach (var metadata in requestDelegateResult.EndpointMetadata) { builder.Metadata.Add(metadata); } From 38790aedb18f8a13d00d5a4a4858115d159e9f9b Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 20 Aug 2021 11:47:08 -0700 Subject: [PATCH 27/29] nit --- .../src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 1eae726c682d..d209bd774a57 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -206,7 +206,7 @@ public static MinimalActionEndpointConventionBuilder Map( // Add delegate attributes as metadata var attributes = action.Method.GetCustomAttributes(); - //Add add request delegate metadata + // Add add request delegate metadata foreach (var metadata in requestDelegateResult.EndpointMetadata) { builder.Metadata.Add(metadata); From 8a8348e333ac8b0153dffbbaa68a4fbf2c25f7eb Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 20 Aug 2021 13:11:12 -0700 Subject: [PATCH 28/29] fix duplicate media types --- .../EndpointMetadataApiDescriptionProvider.cs | 34 ------------------- ...pointMetadataApiDescriptionProviderTest.cs | 31 +---------------- 2 files changed, 1 insertion(+), 64 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 2c5efb6c9f11..815d1b269259 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -124,7 +124,6 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string } AddSupportedRequestFormats(apiDescription.SupportedRequestFormats, hasJsonBody, routeEndpoint.Metadata); - AddAcceptsRequestFormats(apiDescription.SupportedRequestFormats, routeEndpoint.Metadata); AddSupportedResponseTypes(apiDescription.SupportedResponseTypes, methodInfo.ReturnType, routeEndpoint.Metadata); AddActionDescriptorEndpointMetadata(apiDescription.ActionDescriptor, routeEndpoint.Metadata); @@ -235,25 +234,6 @@ private static void AddSupportedRequestFormats( } } - private static void AddAcceptsRequestFormats( - IList supportedRequestFormats, - EndpointMetadataCollection endpointMetadata) - { - var requestMetadata = endpointMetadata.GetOrderedMetadata(); - var declaredContentTypes = GetAcceptsContentTypes(requestMetadata); - - if (declaredContentTypes.Count > 0) - { - foreach (var contentType in declaredContentTypes) - { - supportedRequestFormats.Add(new ApiRequestFormat - { - MediaType = contentType, - }); - } - } - } - private static void AddSupportedResponseTypes( IList supportedResponseTypes, Type returnType, @@ -373,20 +353,6 @@ private static void AddResponseContentTypes(IList apiResponse } } - private static IReadOnlyList GetAcceptsContentTypes(IReadOnlyList? requestMetadataAttributes) - { - var contentTypes = new List(); - if (requestMetadataAttributes != null) - { - foreach (var metadataAttribute in requestMetadataAttributes) - { - contentTypes.AddRange(metadataAttribute.ContentTypes); - } - } - - return contentTypes; - } - private static void AddActionDescriptorEndpointMetadata( ActionDescriptor actionDescriptor, EndpointMetadataCollection endpointMetadata) diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index ae359219bc57..566424670f3e 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -610,7 +610,7 @@ public void HandleAcceptsMetadata() context.Results.SelectMany(r => r.SupportedRequestFormats), requestType => { - Assert.Equal("application/json" , requestType.MediaType); + Assert.Equal("application/json", requestType.MediaType); }, requestType => { @@ -618,35 +618,6 @@ public void HandleAcceptsMetadata() }); } - [Fact] - public void HandleAcceptsMetadataWhenEmptyReturnsDefaultContentType() - { - // Arrange - var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); - builder.MapPost("/api/todos", () => "") - .Accepts(typeof(string), ""); - var context = new ApiDescriptionProviderContext(Array.Empty()); - - var endpointDataSource = builder.DataSources.OfType().Single(); - var hostEnvironment = new HostEnvironment - { - ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) - }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); - - // Act - provider.OnProvidersExecuting(context); - provider.OnProvidersExecuted(context); - - // Assert - Assert.Collection( - context.Results.SelectMany(r => r.SupportedRequestFormats), - requestType => - { - Assert.Equal("application/json", requestType.MediaType); - }); - } - private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) { return apiResponseType.ApiResponseFormats From b29ea2af4ceb900018b882b0699c6b3bfb652622 Mon Sep 17 00:00:00 2001 From: Rafiki Assumani Date: Fri, 20 Aug 2021 15:37:39 -0700 Subject: [PATCH 29/29] fix test failures --- src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index f4730a8f8f86..4dc8330792b4 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -2227,8 +2227,9 @@ public async Task TreatsUnknownNullabilityAsOptionalForReferenceType(bool provid }); } - var requestDelegate = RequestDelegateFactory.Create(optionalQueryParam); + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode);