/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
3 894 строки
154 KB
Sergey
Added string arrays to simple binding (#63072)
19 авг 2025, 02:28
Не верифицирован
19 авг 2025, 02:28
334fe40
Код
Авторство
О чём код?
// 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.Buffers; using System.Globalization; using System.IO.Pipelines; using System.Linq.Expressions; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Numerics; using System.Reflection; using System.Reflection.Metadata; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Generators.Tests; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.InternalTesting; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Routing.Internal; public partial class RequestDelegateFactoryTests : LoggedTest { public static IEnumerable<object[]> NoResult { get { void TestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); } Task TaskTestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); return Task.CompletedTask; } ValueTask ValueTaskTestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); return ValueTask.CompletedTask; } void StaticTestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); } Task StaticTaskTestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); return Task.CompletedTask; } ValueTask StaticValueTaskTestAction(HttpContext httpContext) { MarkAsInvoked(httpContext); return ValueTask.CompletedTask; } void MarkAsInvoked(HttpContext httpContext) { httpContext.Items.Add("invoked", true); } return new List<object[]> { new object[] { (Action<HttpContext>)TestAction }, new object[] { (Func<HttpContext, Task>)TaskTestAction }, new object[] { (Func<HttpContext, ValueTask>)ValueTaskTestAction }, new object[] { (Action<HttpContext>)StaticTestAction }, new object[] { (Func<HttpContext, Task>)StaticTaskTestAction }, new object[] { (Func<HttpContext, ValueTask>)StaticValueTaskTestAction }, }; } } [Theory] [MemberData(nameof(NoResult))] public async Task RequestDelegateInvokesAction(Delegate @delegate) { var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { var response = await next(context); Assert.IsType<EmptyHttpResult>(response); return response; } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.True(httpContext.Items["invoked"] as bool?); } private static void StaticTestActionBasicReflection(HttpContext httpContext) { httpContext.Items.Add("invoked", true); } [Fact] public async Task StaticMethodInfoOverloadWorksWithBasicReflection() { var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( nameof(StaticTestActionBasicReflection), BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); var factoryResult = RequestDelegateFactory.Create(methodInfo!); var requestDelegate = factoryResult.RequestDelegate; var httpContext = CreateHttpContext(); await requestDelegate(httpContext); Assert.True(httpContext.Items["invoked"] as bool?); } private class TestNonStaticActionClass { private readonly object _invokedValue; public TestNonStaticActionClass(object invokedValue) { _invokedValue = invokedValue; } public void NonStaticTestAction(HttpContext httpContext) { httpContext.Items.Add("invoked", _invokedValue); } } [Fact] public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection() { var methodInfo = typeof(TestNonStaticActionClass).GetMethod( nameof(TestNonStaticActionClass.NonStaticTestAction), BindingFlags.Public | BindingFlags.Instance, new[] { typeof(HttpContext) }); var invoked = false; object GetTarget() { if (!invoked) { invoked = true; return new TestNonStaticActionClass(1); } return new TestNonStaticActionClass(2); } var factoryResult = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); var requestDelegate = factoryResult.RequestDelegate; var httpContext = CreateHttpContext(); await requestDelegate(httpContext); Assert.Equal(1, httpContext.Items["invoked"]); httpContext = CreateHttpContext(); await requestDelegate(httpContext); Assert.Equal(2, httpContext.Items["invoked"]); } [Fact] public void BuildRequestDelegateThrowsArgumentNullExceptions() { var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( nameof(StaticTestActionBasicReflection), BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); var serviceProvider = new EmptyServiceProvider(); var exNullAction = Assert.Throws<ArgumentNullException>(() => RequestDelegateFactory.Create(handler: null!)); var exNullMethodInfo1 = Assert.Throws<ArgumentNullException>(() => RequestDelegateFactory.Create(methodInfo: null!)); Assert.Equal("handler", exNullAction.ParamName); Assert.Equal("methodInfo", exNullMethodInfo1.ParamName); } private static void TestOptional(HttpContext httpContext, [FromRoute] int value = 42) { httpContext.Items.Add("input", value); } private static void TestOptionalNullable(HttpContext httpContext, int? value = 42) { httpContext.Items.Add("input", value); } private static void TestOptionalNullableNull(HttpContext httpContext, double? value = null) { httpContext.Items.Add("input", (object?)value ?? "Null"); } private static void TestOptionalString(HttpContext httpContext, string value = "default") { httpContext.Items.Add("input", value); } [Fact] public async Task NullRouteParametersPrefersRouteOverQueryString() { var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { if (id is not null) { httpContext.Items["input"] = id; } }, new() { RouteParameterNames = null }); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["id"] = "41" }); httpContext.Request.RouteValues = new() { ["id"] = "42" }; var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(42, httpContext.Items["input"]); } [Fact] public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() { var methodInfo = typeof(HttpHandler).GetMethod(nameof(HttpHandler.Handle)); Assert.NotNull(methodInfo); var factoryResult = RequestDelegateFactory.Create(methodInfo!); var requestDelegate = factoryResult.RequestDelegate; var context = CreateHttpContext(); await requestDelegate(context); Assert.Equal(1, context.Items["calls"]); await requestDelegate(context); Assert.Equal(1, context.Items["calls"]); } [Fact] public void SpecifiedEmptyRouteParametersThrowIfRouteParameterDoesNotExist() { var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(([FromRoute] int id) => { }, new() { RouteParameterNames = Array.Empty<string>() })); Assert.Equal("'id' is not a route parameter.", ex.Message); } public static object?[][] TryParsableArrayParameters { get { static void Store<T>(HttpContext httpContext, T tryParsable) { httpContext.Items["tryParsable"] = tryParsable; } var now = DateTime.Now; return new[] { // string is not technically "TryParsable", but it's the special case. new object[] { (Action<HttpContext, string[]>)Store, new[] { "plain string" }, new[] { "plain string" } }, new object[] { (Action<HttpContext, StringValues>)Store, new[] { "1", "2", "3" }, new StringValues(new[] { "1", "2", "3" }) }, new object[] { (Action<HttpContext, int[]>)Store, new[] { "-1", "2", "3" }, new[] { -1,2,3 } }, new object[] { (Action<HttpContext, uint[]>)Store, new[] { "1","42","32"}, new[] { 1U, 42U, 32U } }, new object[] { (Action<HttpContext, bool[]>)Store, new[] { "true", "false" }, new[] { true, false } }, new object[] { (Action<HttpContext, short[]>)Store, new[] { "-42" }, new[] { (short)-42 } }, new object[] { (Action<HttpContext, ushort[]>)Store, new[] { "42" }, new[] { (ushort)42 } }, new object[] { (Action<HttpContext, long[]>)Store, new[] { "-42" }, new[] { -42L } }, new object[] { (Action<HttpContext, ulong[]>)Store, new[] { "42" }, new[] { 42UL } }, new object[] { (Action<HttpContext, IntPtr[]>)Store, new[] { "-42" },new[] { new IntPtr(-42) } }, new object[] { (Action<HttpContext, char[]>)Store, new[] { "A" }, new[] { 'A' } }, new object[] { (Action<HttpContext, double[]>)Store, new[] { "0.5" },new[] { 0.5 } }, new object[] { (Action<HttpContext, float[]>)Store, new[] { "0.5" },new[] { 0.5f } }, new object[] { (Action<HttpContext, Half[]>)Store, new[] { "0.5" }, new[] { (Half)0.5f } }, new object[] { (Action<HttpContext, decimal[]>)Store, new[] { "0.5" },new[] { 0.5m } }, new object[] { (Action<HttpContext, Uri[]>)Store, new[] { "https://example.org" }, new[] { new Uri("https://example.org") } }, new object[] { (Action<HttpContext, DateTime[]>)Store, new[] { now.ToString("o") },new[] { now.ToUniversalTime() } }, new object[] { (Action<HttpContext, DateTimeOffset[]>)Store, new[] { "1970-01-01T00:00:00.0000000+00:00" },new[] { DateTimeOffset.UnixEpoch } }, new object[] { (Action<HttpContext, TimeSpan[]>)Store, new[] { "00:00:42" },new[] { TimeSpan.FromSeconds(42) } }, new object[] { (Action<HttpContext, Guid[]>)Store, new[] { "00000000-0000-0000-0000-000000000000" },new[] { Guid.Empty } }, new object[] { (Action<HttpContext, Version[]>)Store, new[] { "6.0.0.42" }, new[] { new Version("6.0.0.42") } }, new object[] { (Action<HttpContext, BigInteger[]>)Store, new[] { "-42" },new[]{ new BigInteger(-42) } }, new object[] { (Action<HttpContext, IPAddress[]>)Store, new[] { "127.0.0.1" }, new[] { IPAddress.Loopback } }, new object[] { (Action<HttpContext, IPEndPoint[]>)Store, new[] { "127.0.0.1:80" },new[] { new IPEndPoint(IPAddress.Loopback, 80) } }, new object[] { (Action<HttpContext, AddressFamily[]>)Store, new[] { "Unix" },new[] { AddressFamily.Unix } }, new object[] { (Action<HttpContext, ILOpCode[]>)Store, new[] { "Nop" }, new[] { ILOpCode.Nop } }, new object[] { (Action<HttpContext, AssemblyFlags[]>)Store, new[] { "PublicKey,Retargetable" },new[] { AssemblyFlags.PublicKey | AssemblyFlags.Retargetable } }, new object[] { (Action<HttpContext, int?[]>)Store, new[] { "42" }, new int?[] { 42 } }, new object[] { (Action<HttpContext, MyEnum[]>)Store, new[] { "ValueB" },new[] { MyEnum.ValueB } }, new object[] { (Action<HttpContext, MyTryParseRecord[]>)Store, new[] { "https://example.org" },new[] { new MyTryParseRecord(new Uri("https://example.org")) } }, new object?[] { (Action<HttpContext, int[]>)Store, new string[] {}, Array.Empty<int>() }, new object?[] { (Action<HttpContext, int?[]>)Store, new string?[] { "1", "2", null, "4" }, new int?[] { 1,2, null, 4 } }, new object?[] { (Action<HttpContext, int?[]>)Store, new string[] { "1", "2", "", "4" }, new int?[] { 1,2, null, 4 } }, new object[] { (Action<HttpContext, MyTryParseRecord?[]?>)Store, new[] { "" }, new MyTryParseRecord?[] { null } }, }; } } public static object?[][] TryParsableParameters { get { static void Store<T>(HttpContext httpContext, T tryParsable) { httpContext.Items["tryParsable"] = tryParsable; } var now = DateTime.Now; return new[] { // string is not technically "TryParsable", but it's the special case. new object[] { (Action<HttpContext, string>)Store, "plain string", "plain string" }, new object[] { (Action<HttpContext, int>)Store, "-42", -42 }, new object[] { (Action<HttpContext, uint>)Store, "42", 42U }, new object[] { (Action<HttpContext, bool>)Store, "true", true }, new object[] { (Action<HttpContext, short>)Store, "-42", (short)-42 }, new object[] { (Action<HttpContext, ushort>)Store, "42", (ushort)42 }, new object[] { (Action<HttpContext, long>)Store, "-42", -42L }, new object[] { (Action<HttpContext, ulong>)Store, "42", 42UL }, new object[] { (Action<HttpContext, IntPtr>)Store, "-42", new IntPtr(-42) }, new object[] { (Action<HttpContext, char>)Store, "A", 'A' }, new object[] { (Action<HttpContext, double>)Store, "0.5", 0.5 }, new object[] { (Action<HttpContext, float>)Store, "0.5", 0.5f }, new object[] { (Action<HttpContext, Half>)Store, "0.5", (Half)0.5f }, new object[] { (Action<HttpContext, decimal>)Store, "0.5", 0.5m }, new object[] { (Action<HttpContext, Uri>)Store, "https://example.org", new Uri("https://example.org") }, new object[] { (Action<HttpContext, DateTime>)Store, now.ToString("o"), now.ToUniversalTime() }, new object[] { (Action<HttpContext, DateTimeOffset>)Store, "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch }, new object[] { (Action<HttpContext, TimeSpan>)Store, "00:00:42", TimeSpan.FromSeconds(42) }, new object[] { (Action<HttpContext, Guid>)Store, "00000000-0000-0000-0000-000000000000", Guid.Empty }, new object[] { (Action<HttpContext, Version>)Store, "6.0.0.42", new Version("6.0.0.42") }, new object[] { (Action<HttpContext, BigInteger>)Store, "-42", new BigInteger(-42) }, new object[] { (Action<HttpContext, IPAddress>)Store, "127.0.0.1", IPAddress.Loopback }, new object[] { (Action<HttpContext, IPEndPoint>)Store, "127.0.0.1:80", new IPEndPoint(IPAddress.Loopback, 80) }, new object[] { (Action<HttpContext, AddressFamily>)Store, "Unix", AddressFamily.Unix }, new object[] { (Action<HttpContext, ILOpCode>)Store, "Nop", ILOpCode.Nop }, new object[] { (Action<HttpContext, AssemblyFlags>)Store, "PublicKey,Retargetable", AssemblyFlags.PublicKey | AssemblyFlags.Retargetable }, new object[] { (Action<HttpContext, int?>)Store, "42", 42 }, new object[] { (Action<HttpContext, MyEnum>)Store, "ValueB", MyEnum.ValueB }, new object[] { (Action<HttpContext, MyTryParseRecord>)Store, "https://example.org", new MyTryParseRecord(new Uri("https://example.org")) }, new object?[] { (Action<HttpContext, int?>)Store, null, null }, }; } } private enum MyEnum { ValueA, ValueB, } private record MyTryParseRecord(Uri Uri) { public static bool TryParse(string? value, out MyTryParseRecord? result) { if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) { result = null; return false; } result = new MyTryParseRecord(uri); return true; } } private record MyBindAsyncRecord(Uri Uri) { public static ValueTask<MyBindAsyncRecord?> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.Equal(typeof(MyBindAsyncRecord), parameter.ParameterType); Assert.StartsWith("myBindAsyncRecord", parameter.Name); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { return new(result: null); } return new(result: new(uri)); } // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's // no [FromRoute] or [FromQuery] attributes. public static bool TryParse(string? value, out MyBindAsyncRecord? result) => throw new NotImplementedException(); } private record struct MyNullableBindAsyncStruct(Uri Uri) { public static ValueTask<MyNullableBindAsyncStruct?> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.True(parameter.ParameterType == typeof(MyNullableBindAsyncStruct) || parameter.ParameterType == typeof(MyNullableBindAsyncStruct?)); Assert.Equal("myNullableBindAsyncStruct", parameter.Name); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { return new(result: null); } return new(result: new(uri)); } } private record struct MyBindAsyncStruct(Uri Uri) { public static ValueTask<MyBindAsyncStruct> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.True(parameter.ParameterType == typeof(MyBindAsyncStruct) || parameter.ParameterType == typeof(MyBindAsyncStruct?)); Assert.Equal("myBindAsyncStruct", parameter.Name); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { throw new BadHttpRequestException("The request is missing the required Referer header."); } return new(result: new(uri)); } // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's // no [FromRoute] or [FromQuery] attributes. public static bool TryParse(string? value, out MyBindAsyncStruct result) => throw new NotImplementedException(); } private record MyAwaitedBindAsyncRecord(Uri Uri) { public static async ValueTask<MyAwaitedBindAsyncRecord?> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.Equal(typeof(MyAwaitedBindAsyncRecord), parameter.ParameterType); Assert.StartsWith("myAwaitedBindAsyncRecord", parameter.Name); await Task.Yield(); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { return null; } return new(uri); } } private record struct MyAwaitedBindAsyncStruct(Uri Uri) { public static async ValueTask<MyAwaitedBindAsyncStruct> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.Equal(typeof(MyAwaitedBindAsyncStruct), parameter.ParameterType); Assert.Equal("myAwaitedBindAsyncStruct", parameter.Name); await Task.Yield(); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { throw new BadHttpRequestException("The request is missing the required Referer header."); } return new(uri); } } private record struct MyBothBindAsyncStruct(Uri Uri) { public static ValueTask<MyBothBindAsyncStruct> BindAsync(HttpContext context, ParameterInfo parameter) { Assert.True(parameter.ParameterType == typeof(MyBothBindAsyncStruct) || parameter.ParameterType == typeof(MyBothBindAsyncStruct?)); Assert.Equal("myBothBindAsyncStruct", parameter.Name); if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { throw new BadHttpRequestException("The request is missing the required Referer header."); } return new(result: new(uri)); } // BindAsync with ParameterInfo is preferred public static ValueTask<MyBothBindAsyncStruct> BindAsync(HttpContext context) { throw new NotImplementedException(); } } private record struct MySimpleBindAsyncStruct(Uri Uri) { public static ValueTask<MySimpleBindAsyncStruct> BindAsync(HttpContext context) { if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { throw new BadHttpRequestException("The request is missing the required Referer header."); } return new(result: new(uri)); } } private record MySimpleBindAsyncRecord(Uri Uri) { public static ValueTask<MySimpleBindAsyncRecord?> BindAsync(HttpContext context) { if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { return new(result: null); } return new(result: new(uri)); } } private interface IBindAsync<T> { static ValueTask<T?> BindAsync(HttpContext context) { if (typeof(T) != typeof(MyBindAsyncFromInterfaceRecord)) { throw new InvalidOperationException(); } if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { return new(default(T)); } return new(result: (T)(object)new MyBindAsyncFromInterfaceRecord(uri)); } } private record MyBindAsyncFromInterfaceRecord(Uri uri) : IBindAsync<MyBindAsyncFromInterfaceRecord> { } [Theory] [MemberData(nameof(TryParsableArrayParameters))] public async Task RequestDelegateHandlesDoesNotHandleArraysFromQueryStringWhenBodyIsInferred(Delegate action, string[]? queryValues, object? expectedParameterValue) { var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["tryParsable"] = queryValues }); var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); // Assert.NotEmpty(httpContext.Items); Assert.Null(httpContext.Items["tryParsable"]); // Ignore this parameter but we want to reuse the dataset GC.KeepAlive(expectedParameterValue); } [Fact] public async Task RequestDelegateHandlesOptionalArraysFromNullQueryString() { var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["tryParsable"] = (string?)null }); static void StoreNullableIntArray(HttpContext httpContext, int?[]? tryParsable) { httpContext.Items["tryParsable"] = tryParsable; } var factoryResult = RequestDelegateFactory.Create(StoreNullableIntArray, new() { DisableInferBodyFromParameters = true }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.NotEmpty(httpContext.Items); Assert.Null(httpContext.Items["tryParsable"]); } [Fact] public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStringSource() { var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["a"] = new(new[] { "1", "2", "3" }) }); httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues> { ["form"] = new(new[] { "7", "8", "9" }) }); var factoryResult = RequestDelegateFactory.Create((HttpContext context, [FromHeader(Name = "Custom")] StringValues? headerValues, [FromQuery(Name = "a")] StringValues? queryValues, [FromForm(Name = "form")] StringValues? formValues) => { context.Items["headers"] = headerValues; context.Items["query"] = queryValues; context.Items["form"] = formValues; }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(new StringValues(new[] { "1", "2", "3" }), httpContext.Items["query"]); Assert.Equal(new StringValues(new[] { "4", "5", "6" }), httpContext.Items["headers"]); Assert.Equal(new StringValues(new[] { "7", "8", "9" }), httpContext.Items["form"]!); } [Fact] public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStringSourceForUnpresentedValues() { var httpContext = CreateHttpContext(); httpContext.Request.Form = new FormCollection(null); var factoryResult = RequestDelegateFactory.Create((HttpContext context, [FromHeader(Name = "foo")] StringValues? headerValues, [FromQuery(Name = "bar")] StringValues? queryValues, [FromForm(Name = "form")] StringValues? formValues) => { Assert.False(headerValues.HasValue); Assert.False(queryValues.HasValue); Assert.False(formValues.HasValue); context.Items["headers"] = headerValues; context.Items["query"] = queryValues; context.Items["form"] = formValues; }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Null(httpContext.Items["query"]); Assert.Null(httpContext.Items["headers"]); Assert.Null(httpContext.Items["form"]); } [Fact] public async Task RequestDelegateHandlesFromFormStringArrayParameter() { var httpContext = CreateHttpContext(); httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues> { ["form"] = new(new[] { "1", "2", "3" }) }); var factoryResult = RequestDelegateFactory.Create( (HttpContext context, [FromForm(Name = "form")] string[] formValues) => { context.Items["form"] = formValues; }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var parameterBindingMetadata = factoryResult.EndpointMetadata .FirstOrDefault(e => e is ParameterBindingMetadata metadata && metadata.Name == "formValues") as ParameterBindingMetadata; Assert.NotNull(parameterBindingMetadata); Assert.Equal(typeof(string[]), parameterBindingMetadata.ParameterInfo.ParameterType); Assert.Equal(new StringValues(new[] { "1", "2", "3" }), httpContext.Items["form"]!); } [Fact] public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyCompleted() { var httpContext = CreateHttpContext(); httpContext.Request.Headers.Referer = "https://example.org"; var resultFactory = RequestDelegateFactory.Create( (HttpContext httpContext, MyAwaitedBindAsyncRecord myAwaitedBindAsyncRecord, MyAwaitedBindAsyncStruct myAwaitedBindAsyncStruct) => { httpContext.Items["myAwaitedBindAsyncRecord"] = myAwaitedBindAsyncRecord; httpContext.Items["myAwaitedBindAsyncStruct"] = myAwaitedBindAsyncStruct; }); var requestDelegate = resultFactory.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(new MyAwaitedBindAsyncRecord(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncRecord"]); Assert.Equal(new MyAwaitedBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncStruct"]); } public static object[][] DelegatesWithAttributesOnNotTryParsableParameters { get { void InvalidFromRoute([FromRoute] object notTryParsable) { } void InvalidFromQuery([FromQuery] object notTryParsable) { } void InvalidFromHeader([FromHeader] object notTryParsable) { } return new[] { new object[] { (Action<object>)InvalidFromRoute }, new object[] { (Action<object>)InvalidFromQuery }, new object[] { (Action<object>)InvalidFromHeader }, }; } } [Theory] [MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))] public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) { var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(action)); Assert.Equal("notTryParsable must have a valid TryParse method to support converting from a string. No public static bool object.TryParse(string, out object) method found for notTryParsable.", ex.Message); } [Fact] public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() { var unnamedParameter = Expression.Parameter(typeof(int)); var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(lambda.Compile())); Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message); } [Fact] public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequestWithNonOptionalArrays() { var invoked = false; void StoreNullableIntArray(HttpContext httpContext, int?[] values) { invoked = true; } var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>() { ["values"] = (string?)null }); var factoryResult = RequestDelegateFactory.Create(StoreNullableIntArray, new() { ThrowOnBadRequest = true, DisableInferBodyFromParameters = true }); var requestDelegate = factoryResult.RequestDelegate; var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext)); Assert.False(invoked); // The httpContext should be untouched. Assert.False(httpContext.RequestAborted.IsCancellationRequested); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.Response.HasStarted); // We don't log bad requests when we throw. Assert.Empty(TestSink.Writes); Assert.Equal(@"Failed to bind parameter ""Nullable<int>[] values"" from """".", badHttpRequestException.Message); Assert.Equal(400, badHttpRequestException.StatusCode); } private record ParametersListWithImplictFromBody(HttpContext HttpContext, TodoStruct Todo); private record ParametersListWithExplictFromBody(HttpContext HttpContext, [FromBody] Todo Todo); public static object[][] ImplicitFromBodyActions { get { void TestImpliedFromBody(HttpContext httpContext, Todo todo) { httpContext.Items.Add("body", todo); } void TestImpliedFromBodyInterface(HttpContext httpContext, ITodo todo) { httpContext.Items.Add("body", todo); } void TestImpliedFromBodyStruct(HttpContext httpContext, TodoStruct todo) { httpContext.Items.Add("body", todo); } void TestImpliedFromBodyStruct_ParameterList([AsParameters] ParametersListWithImplictFromBody args) { args.HttpContext.Items.Add("body", args.Todo); } return new[] { new[] { (Action<HttpContext, Todo>)TestImpliedFromBody }, new[] { (Action<HttpContext, ITodo>)TestImpliedFromBodyInterface }, new object[] { (Action<HttpContext, TodoStruct>)TestImpliedFromBodyStruct }, new object[] { (Action<ParametersListWithImplictFromBody>)TestImpliedFromBodyStruct_ParameterList }, }; } } public static object[][] ExplicitFromBodyActions { get { void TestExplicitFromBody_ParameterList([AsParameters] ParametersListWithExplictFromBody args) { args.HttpContext.Items.Add("body", args.Todo); } return new[] { new object[] { (Action<ParametersListWithExplictFromBody>)TestExplicitFromBody_ParameterList }, }; } } public static object[][] FromBodyActions { get { return ExplicitFromBodyActions.Concat(ImplicitFromBodyActions).ToArray(); } } [Theory] [MemberData(nameof(FromBodyActions))] public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) { Todo originalTodo = new() { Name = "Write more tests!" }; var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); var stream = new MemoryStream(requestBodyBytes); httpContext.Request.Body = stream; httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true)); var jsonOptions = new JsonOptions(); jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); var mock = new Mock<IServiceProvider>(); mock.Setup(m => m.GetService(It.IsAny<Type>())).Returns<Type>(t => { if (t == typeof(IOptions<JsonOptions>)) { return Options.Create(jsonOptions); } return null; }); httpContext.RequestServices = mock.Object; var factoryResult = RequestDelegateFactory.Create(action, new RequestDelegateFactoryOptions() { ServiceProvider = mock.Object }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var deserializedRequestBody = httpContext.Items["body"]; Assert.NotNull(deserializedRequestBody); Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name); } [Theory] [MemberData(nameof(ExplicitFromBodyActions))] public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter(Delegate action) { var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(false)); var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(400, httpContext.Response.StatusCode); } [Fact] public void RequestDelegateFactoryThrowsForByRefReturnTypes() { ReadOnlySpan<byte> Method1() => "hello world"u8; Span<byte> Method2() => "hello world"u8.ToArray(); RefStruct Method3() => new("hello world"u8); var ex1 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method1)); var ex2 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method2)); var ex3 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method3)); Assert.Equal("Unsupported return type: System.ReadOnlySpan<byte>", ex1.Message); Assert.Equal("Unsupported return type: System.Span<byte>", ex2.Message); Assert.Equal($"Unsupported return type: {typeof(RefStruct).FullName}", ex3.Message); } ref struct RefStruct { public ReadOnlySpan<byte> Buffer { get; } public RefStruct(ReadOnlySpan<byte> buffer) { Buffer = buffer; } } [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() { void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { } void TestInferredInvalidAction(Todo value1, Todo value2) { } void TestBothInvalidAction(Todo value1, [FromBody] int value2) { } Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestAttributedInvalidAction)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestInferredInvalidAction)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestBothInvalidAction)); } [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidTryParse() { void TestTryParseStruct(BadTryParseStruct value1) { } void TestTryParseClass(BadTryParseClass value1) { } Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestTryParseStruct)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestTryParseClass)); } private struct BadTryParseStruct { public static void TryParse(string? value, out BadTryParseStruct result) { } } private class BadTryParseClass { public static void TryParse(string? value, out BadTryParseClass result) { result = new(); } } [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidBindAsync() { void TestBindAsyncStruct(BadBindAsyncStruct value1) { } void TestBindAsyncClass(BadBindAsyncClass value1) { } var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestBindAsyncStruct)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestBindAsyncClass)); } private struct BadBindAsyncStruct { public static Task<BadBindAsyncStruct> BindAsync(HttpContext context, ParameterInfo parameter) => throw new NotImplementedException(); } private class BadBindAsyncClass { public static Task<BadBindAsyncClass> BindAsync(HttpContext context, ParameterInfo parameter) => throw new NotImplementedException(); } public static object[][] BadArgumentListActions { get { void TestParameterListRecord([AsParameters] BadArgumentListRecord req) { } void TestParameterListClass([AsParameters] BadArgumentListClass req) { } void TestParameterListClassWithMutipleConstructors([AsParameters] BadArgumentListClassMultipleCtors req) { } void TestParameterListAbstractClass([AsParameters] BadAbstractArgumentListClass req) { } void TestParameterListNoPulicConstructorClass([AsParameters] BadNoPublicConstructorArgumentListClass req) { } static string GetMultipleContructorsError(Type type) => $"Only a single public parameterized constructor is allowed for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'."; static string GetAbstractClassError(Type type) => $"The abstract type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}' is not supported."; static string GetNoContructorsError(Type type) => $"No public parameterless constructor found for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'."; static string GetInvalidConstructorError(Type type) => $"The public parameterized constructor must contain only parameters that match the declared public properties for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'."; return new object[][] { new object[] { (Action<BadArgumentListRecord>)TestParameterListRecord, GetMultipleContructorsError(typeof(BadArgumentListRecord)) }, new object[] { (Action<BadArgumentListClass>)TestParameterListClass, GetInvalidConstructorError(typeof(BadArgumentListClass)) }, new object[] { (Action<BadArgumentListClassMultipleCtors>)TestParameterListClassWithMutipleConstructors, GetMultipleContructorsError(typeof(BadArgumentListClassMultipleCtors)) }, new object[] { (Action<BadAbstractArgumentListClass>)TestParameterListAbstractClass, GetAbstractClassError(typeof(BadAbstractArgumentListClass)) }, new object[] { (Action<BadNoPublicConstructorArgumentListClass>)TestParameterListNoPulicConstructorClass, GetNoContructorsError(typeof(BadNoPublicConstructorArgumentListClass)) }, }; } } [Theory] [MemberData(nameof(BadArgumentListActions))] public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidParameterListConstructor( Delegate @delegate, string errorMessage) { var exception = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(@delegate)); Assert.Equal(errorMessage, exception.Message); } [Fact] public void BuildRequestDelegateThrowsNotSupportedExceptionForNestedParametersList() { void TestNestedParameterListRecordOnType([AsParameters] NestedArgumentListRecord req) { } void TestNestedParameterListRecordOnArgument([AsParameters] ClassWithParametersConstructor req) { } Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestNestedParameterListRecordOnType)); Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestNestedParameterListRecordOnArgument)); } private record ParametersListWithImplictFromService(HttpContext HttpContext, IMyService MyService); private record ParametersListWithExplictFromService(HttpContext HttpContext, [FromService] MyService MyService); public static object[][] ExplicitFromServiceActions { get { void TestExplicitFromService(HttpContext httpContext, [FromService] MyService myService) { httpContext.Items.Add("service", myService); } void TestExplicitFromService_FromParameterList([AsParameters] ParametersListWithExplictFromService args) { args.HttpContext.Items.Add("service", args.MyService); } void TestExplicitFromIEnumerableService(HttpContext httpContext, [FromService] IEnumerable<MyService> myServices) { httpContext.Items.Add("service", myServices.Single()); } void TestExplicitMultipleFromService(HttpContext httpContext, [FromService] MyService myService, [FromService] IEnumerable<MyService> myServices) { httpContext.Items.Add("service", myService); } return new object[][] { new[] { (Action<HttpContext, MyService>)TestExplicitFromService }, new object[] { (Action<ParametersListWithExplictFromService>)TestExplicitFromService_FromParameterList }, new[] { (Action<HttpContext, IEnumerable<MyService>>)TestExplicitFromIEnumerableService }, new[] { (Action<HttpContext, MyService, IEnumerable<MyService>>)TestExplicitMultipleFromService }, }; } } public static object[][] ImplicitFromServiceActions { get { void TestImpliedFromService(HttpContext httpContext, IMyService myService) { httpContext.Items.Add("service", myService); } void TestImpliedFromService_FromParameterList([AsParameters] ParametersListWithImplictFromService args) { args.HttpContext.Items.Add("service", args.MyService); } void TestImpliedIEnumerableFromService(HttpContext httpContext, IEnumerable<MyService> myServices) { httpContext.Items.Add("service", myServices.Single()); } void TestImpliedFromServiceBasedOnContainer(HttpContext httpContext, MyService myService) { httpContext.Items.Add("service", myService); } return new object[][] { new[] { (Action<HttpContext, IMyService>)TestImpliedFromService }, new object[] { (Action<ParametersListWithImplictFromService>)TestImpliedFromService_FromParameterList }, new[] { (Action<HttpContext, IEnumerable<MyService>>)TestImpliedIEnumerableFromService }, new[] { (Action<HttpContext, MyService>)TestImpliedFromServiceBasedOnContainer }, }; } } public static object[][] FromServiceActions { get { return ImplicitFromServiceActions.Concat(ExplicitFromServiceActions).ToArray(); } } [Fact] public void BuildRequestDelegateThrowsNotSupportedExceptionForByRefParameters() { void OutMethod(out string foo) { foo = ""; } void InMethod(in string foo) { } void RefMethod(ref string foo) { } var outParamException = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(OutMethod)); var inParamException = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(InMethod)); var refParamException = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(RefMethod)); var typeName = typeof(string).MakeByRefType().Name; Assert.Equal($"The by reference parameter 'out {typeName} foo' is not supported.", outParamException.Message); Assert.Equal($"The by reference parameter 'in {typeName} foo' is not supported.", inParamException.Message); Assert.Equal($"The by reference parameter 'ref {typeName} foo' is not supported.", refParamException.Message); } [Theory] [MemberData(nameof(ImplicitFromServiceActions))] public async Task RequestDelegateRequiresServiceForAllImplicitFromServiceParameters(Delegate action) { var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var message = Assert.Single(TestSink.Writes).Message; Assert.StartsWith("Implicit body inferred for parameter", message); Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", message); } [Theory] [MemberData(nameof(ExplicitFromServiceActions))] public async Task RequestDelegateWithExplicitFromServiceParameters(Delegate action) { // IEnumerable<T> always resolves from DI but is empty and throws from test method if (action.Method.Name.Contains("TestExplicitFromIEnumerableService", StringComparison.Ordinal)) { return; } var httpContext = CreateHttpContext(); var requestDelegateResult = RequestDelegateFactory.Create(action); var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => requestDelegateResult.RequestDelegate(httpContext)); Assert.Equal("No service for type 'Microsoft.AspNetCore.Routing.Internal.RequestDelegateFactoryTests+MyService' has been registered.", ex.Message); } [Theory] [MemberData(nameof(FromServiceActions))] public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action) { var myOriginalService = new MyService(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(LoggerFactory); serviceCollection.AddSingleton(myOriginalService); serviceCollection.AddSingleton<IMyService>(myOriginalService); var services = serviceCollection.BuildServiceProvider(); using var requestScoped = services.CreateScope(); var httpContext = CreateHttpContext(); httpContext.RequestServices = requestScoped.ServiceProvider; var factoryResult = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Same(myOriginalService, httpContext.Items["service"]); } [Fact] public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() { HttpContext? httpContextArgument = null; void TestAction(HttpContext httpContext) { httpContextArgument = httpContext; } var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Same(httpContext, httpContextArgument); } [Fact] public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() { CancellationToken? cancellationTokenArgument = null; void TestAction(CancellationToken cancellationToken) { cancellationTokenArgument = cancellationToken; } using var cts = new CancellationTokenSource(); var httpContext = CreateHttpContext(); // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. httpContext.Features.Set<IHttpRequestLifetimeFeature>(new HttpRequestLifetimeFeature()); httpContext.RequestAborted = cts.Token; var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(httpContext.RequestAborted, cancellationTokenArgument); } [Fact] public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() { ClaimsPrincipal? userArgument = null; void TestAction(ClaimsPrincipal user) { userArgument = user; } var httpContext = CreateHttpContext(); httpContext.User = new ClaimsPrincipal(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(httpContext.User, userArgument); } [Fact] public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() { HttpRequest? httpRequestArgument = null; void TestAction(HttpRequest httpRequest) { httpRequestArgument = httpRequest; } var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(httpContext.Request, httpRequestArgument); } [Fact] public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse() { HttpResponse? httpResponseArgument = null; void TestAction(HttpResponse httpResponse) { httpResponseArgument = httpResponse; } var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(httpContext.Response, httpResponseArgument); } public static IEnumerable<object[]> PolymorphicResult { get { JsonTodoChild originalTodo = new() { Name = "Write even more tests!", Child = "With type hierarchies!", }; JsonTodo TestAction() => originalTodo; Task<JsonTodo> TaskTestAction() => Task.FromResult<JsonTodo>(originalTodo); async Task<JsonTodo> TaskTestActionAwaited() { await Task.Yield(); return originalTodo; } ValueTask<JsonTodo> ValueTaskTestAction() => ValueTask.FromResult<JsonTodo>(originalTodo); async ValueTask<JsonTodo> ValueTaskTestActionAwaited() { await Task.Yield(); return originalTodo; } return new List<object[]> { new object[] { (Func<JsonTodo>)TestAction }, new object[] { (Func<Task<JsonTodo>>)TaskTestAction}, new object[] { (Func<Task<JsonTodo>>)TaskTestActionAwaited}, new object[] { (Func<ValueTask<JsonTodo>>)ValueTaskTestAction}, new object[] { (Func<ValueTask<JsonTodo>>)ValueTaskTestActionAwaited}, }; } } // NOTE: This test needs to be retained here because it tests a specific capability to create a delegate // from the request delegate factory without passing in an instance of a service provider which // is not something we can get at with the shared compiler/runtime test harness. [Theory] [MemberData(nameof(PolymorphicResult))] public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_WithJsonPolymorphicOptions(Delegate @delegate) { var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var deserializedResponseBody = JsonSerializer.Deserialize<JsonTodoChild>(responseBodyStream.ToArray(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); Assert.NotNull(deserializedResponseBody); Assert.Equal("Write even more tests!", deserializedResponseBody!.Name); Assert.Equal("With type hierarchies!", deserializedResponseBody!.Child); } // NOTE: This test needs to be retained here because it tests a specific capability to create a delegate // from the request delegate factory without passing in an instance of a service provider which // is not something we can get at with the shared compiler/runtime test harness. There is a variant // of this test that has been added to the shared test harness to make sure that we do write the // type discriminator. [Theory] [MemberData(nameof(PolymorphicResult))] public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_WithJsonPolymorphicOptions(Delegate @delegate) { var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var deserializedResponseBody = JsonNode.Parse(responseBodyStream.ToArray()); Assert.NotNull(deserializedResponseBody); Assert.NotNull(deserializedResponseBody["$type"]); Assert.Equal(nameof(JsonTodoChild), deserializedResponseBody["$type"]!.GetValue<string>()); } [JsonSerializable(typeof(Todo))] [JsonSerializable(typeof(TodoChild))] private partial class TestJsonContext : JsonSerializerContext { } [Fact] public void CreateDelegateThrows_WhenGetJsonTypeInfoFail() { var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; TodoStruct TestAction() => new TodoStruct(42, "Bob", true); Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction, new() { ServiceProvider = httpContext.RequestServices })); } public static IEnumerable<object[]> CustomResults { get { var resultString = "Still not enough tests!"; CustomResult TestAction() => new CustomResult(resultString); Task<CustomResult> TaskTestAction() => Task.FromResult(new CustomResult(resultString)); ValueTask<CustomResult> ValueTaskTestAction() => ValueTask.FromResult(new CustomResult(resultString)); FSharp.Control.FSharpAsync<CustomResult> FSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CustomResult(resultString)); static CustomResult StaticTestAction() => new CustomResult("Still not enough tests!"); static Task<CustomResult> StaticTaskTestAction() => Task.FromResult(new CustomResult("Still not enough tests!")); static ValueTask<CustomResult> StaticValueTaskTestAction() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); static FSharp.Control.FSharpAsync<CustomResult> StaticFSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CustomResult("Still not enough tests!")); // Object return type where the object is IResult static object StaticResultAsObject() => new CustomResult("Still not enough tests!"); // Task<object> return type static Task<object> StaticTaskOfIResultAsObject() => Task.FromResult<object>(new CustomResult("Still not enough tests!")); static ValueTask<object> StaticValueTaskOfIResultAsObject() => ValueTask.FromResult<object>(new CustomResult("Still not enough tests!")); static FSharp.Control.FSharpAsync<object> StaticFSharpAsyncOfIResultAsObject() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return<object>(new CustomResult("Still not enough tests!")); StructResult TestStructAction() => new StructResult(resultString); Task<StructResult> TaskTestStructAction() => Task.FromResult(new StructResult(resultString)); ValueTask<StructResult> ValueTaskTestStructAction() => ValueTask.FromResult(new StructResult(resultString)); FSharp.Control.FSharpAsync<StructResult> FSharpAsyncTestStructAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new StructResult(resultString)); return new List<object[]> { new object[] { (Func<CustomResult>)TestAction }, new object[] { (Func<Task<CustomResult>>)TaskTestAction}, new object[] { (Func<ValueTask<CustomResult>>)ValueTaskTestAction}, new object[] { (Func<FSharp.Control.FSharpAsync<CustomResult>>)FSharpAsyncTestAction }, new object[] { (Func<CustomResult>)StaticTestAction}, new object[] { (Func<Task<CustomResult>>)StaticTaskTestAction}, new object[] { (Func<ValueTask<CustomResult>>)StaticValueTaskTestAction}, new object[] { (Func<FSharp.Control.FSharpAsync<CustomResult>>)StaticFSharpAsyncTestAction }, new object[] { (Func<object>)StaticResultAsObject}, new object[] { (Func<Task<object>>)StaticTaskOfIResultAsObject}, new object[] { (Func<ValueTask<object>>)StaticValueTaskOfIResultAsObject}, new object[] { (Func<FSharp.Control.FSharpAsync<object>>)StaticFSharpAsyncOfIResultAsObject}, new object[] { (Func<StructResult>)TestStructAction }, new object[] { (Func<Task<StructResult>>)TaskTestStructAction }, new object[] { (Func<ValueTask<StructResult>>)ValueTaskTestStructAction }, new object[] { (Func<FSharp.Control.FSharpAsync<StructResult>>)FSharpAsyncTestStructAction }, }; } } [Theory] [MemberData(nameof(CustomResults))] public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) { var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal("Still not enough tests!", decodedResponseBody); } public static IEnumerable<object[]> NullResult { get { IResult? TestAction() => null; Task<bool?>? TaskBoolAction() => null; Task<IResult?>? TaskNullAction() => null; Task<IResult?> TaskTestAction() => Task.FromResult<IResult?>(null); ValueTask<IResult?> ValueTaskTestAction() => ValueTask.FromResult<IResult?>(null); return new List<object[]> { new object[] { (Func<IResult?>)TestAction, "The IResult returned by the Delegate must not be null." }, new object[] { (Func<Task<IResult?>?>)TaskNullAction, "The IResult in Task<IResult> response must not be null." }, new object[] { (Func<Task<bool?>?>)TaskBoolAction, "The Task returned by the Delegate must not be null." }, new object[] { (Func<Task<IResult?>>)TaskTestAction, "The IResult returned by the Delegate must not be null." }, new object[] { (Func<ValueTask<IResult?>>)ValueTaskTestAction, "The IResult returned by the Delegate must not be null." }, }; } } [Theory] [MemberData(nameof(NullResult))] public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(Delegate @delegate, string message) { var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; var exception = await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await requestDelegate(httpContext)); Assert.Contains(message, exception.Message); } public static IEnumerable<object?[]> RouteParamOptionalityData { get { string requiredRouteParam(string name) => $"Hello {name}!"; string defaultValueRouteParam(string name = "DefaultName") => $"Hello {name}!"; string nullableRouteParam(string? name) => $"Hello {name}!"; string requiredParseableRouteParam(int age) => $"Age: {age}"; string defaultValueParseableRouteParam(int age = 12) => $"Age: {age}"; string nullableParseableRouteParam(int? age) => $"Age: {age}"; return new List<object?[]> { new object?[] { (Func<string, string>)requiredRouteParam, "name", null, true, null}, new object?[] { (Func<string, string>)requiredRouteParam, "name", "TestName", false, "Hello TestName!" }, new object?[] { (Func<string, string>)defaultValueRouteParam, "name", null, false, "Hello DefaultName!" }, new object?[] { (Func<string, string>)defaultValueRouteParam, "name", "TestName", false, "Hello TestName!" }, new object?[] { (Func<string?, string>)nullableRouteParam, "name", null, false, "Hello !" }, new object?[] { (Func<string?, string>)nullableRouteParam, "name", "TestName", false, "Hello TestName!" }, new object?[] { (Func<int, string>)requiredParseableRouteParam, "age", null, true, null}, new object?[] { (Func<int, string>)requiredParseableRouteParam, "age", "42", false, "Age: 42" }, new object?[] { (Func<int, string>)defaultValueParseableRouteParam, "age", null, false, "Age: 12" }, new object?[] { (Func<int, string>)defaultValueParseableRouteParam, "age", "42", false, "Age: 42" }, new object?[] { (Func<int?, string>)nullableParseableRouteParam, "age", null, false, "Age: " }, new object?[] { (Func<int?, string>)nullableParseableRouteParam, "age", "42", false, "Age: 42"}, }; } } [Theory] [MemberData(nameof(RouteParamOptionalityData))] public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate, string paramName, string? routeParam, bool isInvalid, string? expectedResponse) { var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; if (routeParam is not null) { httpContext.Request.RouteValues[paramName] = routeParam; } var factoryResult = RequestDelegateFactory.Create(@delegate, new() { RouteParameterNames = routeParam is not null ? new[] { paramName } : Array.Empty<string>() }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); var logs = TestSink.Writes.ToArray(); if (isInvalid) { Assert.Equal(400, httpContext.Response.StatusCode); var log = Assert.Single(logs); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); var expectedType = paramName == "age" ? "int age" : $"string name"; Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from query string.", log.Message); } else { Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(expectedResponse, decodedResponseBody); } } public static IEnumerable<object?[]> BindAsyncParamOptionalityData { get { void requiredReferenceType(HttpContext context, MyBindAsyncRecord myBindAsyncRecord) { context.Items["uri"] = myBindAsyncRecord.Uri; } void defaultReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord = null) { context.Items["uri"] = myBindAsyncRecord?.Uri; } void nullableReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord) { context.Items["uri"] = myBindAsyncRecord?.Uri; } void requiredReferenceTypeSimple(HttpContext context, MySimpleBindAsyncRecord mySimpleBindAsyncRecord) { context.Items["uri"] = mySimpleBindAsyncRecord.Uri; } void requiredValueType(HttpContext context, MyNullableBindAsyncStruct myNullableBindAsyncStruct) { context.Items["uri"] = myNullableBindAsyncStruct.Uri; } void defaultValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct = null) { context.Items["uri"] = myNullableBindAsyncStruct?.Uri; } void nullableValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct) { context.Items["uri"] = myNullableBindAsyncStruct?.Uri; } void requiredValueTypeSimple(HttpContext context, MySimpleBindAsyncStruct mySimpleBindAsyncStruct) { context.Items["uri"] = mySimpleBindAsyncStruct.Uri; } return new object?[][] { new object?[] { (Action<HttpContext, MyBindAsyncRecord>)requiredReferenceType, false, true, false }, new object?[] { (Action<HttpContext, MyBindAsyncRecord>)requiredReferenceType, true, false, false, }, new object?[] { (Action<HttpContext, MySimpleBindAsyncRecord>)requiredReferenceTypeSimple, true, false, false }, new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)defaultReferenceType, false, false, false, }, new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)defaultReferenceType, true, false, false }, new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)nullableReferenceType, false, false, false }, new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)nullableReferenceType, true, false, false }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct>)requiredValueType, false, true, true }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct>)requiredValueType, true, false, true }, new object?[] { (Action<HttpContext, MySimpleBindAsyncStruct>)requiredValueTypeSimple, true, false, true }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)defaultValueType, false, false, true }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)defaultValueType, true, false, true }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)nullableValueType, false, false, true }, new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)nullableValueType, true, false, true }, }; } } [Theory] [MemberData(nameof(BindAsyncParamOptionalityData))] public async Task RequestDelegateHandlesBindAsyncOptionality(Delegate routeHandler, bool includeReferer, bool isInvalid, bool isStruct) { var httpContext = CreateHttpContext(); if (includeReferer) { httpContext.Request.Headers.Referer = "https://example.org"; } var factoryResult = RequestDelegateFactory.Create(routeHandler); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.False(httpContext.RequestAborted.IsCancellationRequested); if (isInvalid) { Assert.Equal(400, httpContext.Response.StatusCode); var log = Assert.Single(TestSink.Writes); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); if (isStruct) { Assert.Equal(@"Required parameter ""MyNullableBindAsyncStruct myNullableBindAsyncStruct"" was not provided from MyNullableBindAsyncStruct.BindAsync(HttpContext, ParameterInfo).", log.Message); } else { Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", log.Message); } } else { Assert.Equal(200, httpContext.Response.StatusCode); if (includeReferer) { Assert.Equal(new Uri("https://example.org"), httpContext.Items["uri"]); } else { Assert.Null(httpContext.Items["uri"]); } } } public static IEnumerable<object?[]> ServiceParamOptionalityData { get { string requiredExplicitService([FromService] MyService service) => $"Service: {service}"; string defaultValueExplicitServiceParam([FromService] MyService? service = null) => $"Service: {service}"; string nullableExplicitServiceParam([FromService] MyService? service) => $"Service: {service}"; return new List<object?[]> { new object?[] { (Func<MyService, string>)requiredExplicitService, false, true}, new object?[] { (Func<MyService, string>)requiredExplicitService, true, false}, new object?[] { (Func<MyService, string>)defaultValueExplicitServiceParam, false, false}, new object?[] { (Func<MyService, string>)defaultValueExplicitServiceParam, true, false}, new object?[] { (Func<MyService?, string>)nullableExplicitServiceParam, false, false}, new object?[] { (Func<MyService?, string>)nullableExplicitServiceParam, true, false}, }; } } [Theory] [MemberData(nameof(ServiceParamOptionalityData))] public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delegate, bool hasService, bool isInvalid) { var httpContext = CreateHttpContext(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(LoggerFactory); if (hasService) { var service = new MyService(); serviceCollection.AddSingleton(service); } var services = serviceCollection.BuildServiceProvider(); httpContext.RequestServices = services; RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; var factoryResult = RequestDelegateFactory.Create(@delegate, options); var requestDelegate = factoryResult.RequestDelegate; if (!isInvalid) { await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); } else { await Assert.ThrowsAsync<InvalidOperationException>(() => requestDelegate(httpContext)); Assert.False(httpContext.RequestAborted.IsCancellationRequested); } } #nullable disable [Theory] [InlineData(true, "Hello TestName!")] [InlineData(false, "Hello !")] public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) { string optionalQueryParam(string name = null) => $"Hello {name}!"; var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; if (provideValue) { httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["name"] = "TestName" }); } var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(expectedResponse, decodedResponseBody); } [Theory] [InlineData(true, "Age: 42")] [InlineData(false, "Age: 0")] public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) { string optionalQueryParam(int age = default(int)) => $"Age: {age}"; var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; if (provideValue) { httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["age"] = "42" }); } var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(expectedResponse, decodedResponseBody); } [Theory] [InlineData(true, "Age: 42")] [InlineData(false, "Age: ")] public async Task TreatsUnknownNullabilityAsOptionalForReferenceType(bool provideValue, string expectedResponse) { string optionalQueryParam(string age) => $"Age: {age}"; var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; if (provideValue) { httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["age"] = "42" }); } var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(expectedResponse, decodedResponseBody); } #nullable enable [Fact] public async Task CanExecuteRequestDelegateWithResultsExtension() { IResult actionWithExtensionsResult(string name) => Results.Extensions.TestResult(name); var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["name"] = "Tester" }); var factoryResult = RequestDelegateFactory.Create(actionWithExtensionsResult); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(@"""Hello Tester. This is from an extension method.""", decodedResponseBody); } public static IEnumerable<object?[]> UriDelegates { get { string uriParsing(Uri uri) => $"Uri: {uri.OriginalString}"; return new List<object?[]> { new object?[] { (Func<Uri, string>)uriParsing, "https://example.org", "Uri: https://example.org" }, new object?[] { (Func<Uri, string>)uriParsing, "https://example.org/path/to/file?name=value1&name=value2", "Uri: https://example.org/path/to/file?name=value1&name=value2" }, new object?[] { (Func<Uri, string>)uriParsing, "/path/to/file?name=value1&name=value2", "Uri: /path/to/file?name=value1&name=value2" }, new object?[] { (Func<Uri, string>)uriParsing, "?name=value1&name=value2", "Uri: ?name=value1&name=value2" }, }; } } [Theory] [MemberData(nameof(UriDelegates))] public async Task RequestDelegateCanProcessUriValues(Delegate @delegate, string uri, string expectedResponse) { var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["uri"] = uri }); var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(expectedResponse, decodedResponseBody); } [Fact] public async Task RequestDelegateThrowsBadHttpRequestExceptionWhenReadingOversizeFormResultsIn413BadRequest() { var invoked = false; void TestAction(IFormFile file) { invoked = true; } var exception = new BadHttpRequestException("Request body too large. The max request body size is [who cares] bytes.", 413); var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; httpContext.Request.Headers["Content-Length"] = "1"; httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(exception); httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true)); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.False(invoked); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); Assert.Equal(@"Reading the request body failed with an IOException.", logMessage.Message); Assert.Same(exception, logMessage.Exception); Assert.Equal(413, httpContext.Response.StatusCode); } [Fact] public async Task RequestDelegateThrowsBadHttpRequestExceptionWhenReadingOversizeJsonBodyResultsIn413BadRequest() { var invoked = false; void TestAction(Todo todo) { invoked = true; } var exception = new BadHttpRequestException("Request body too large. The max request body size is [who cares] bytes.", 413); var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1000"; httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(exception); httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true)); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.False(invoked); Assert.False(httpContext.RequestAborted.IsCancellationRequested); var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); Assert.Equal(@"Reading the request body failed with an IOException.", logMessage.Message); Assert.Same(exception, logMessage.Exception); Assert.Equal(413, httpContext.Response.StatusCode); } [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionBodyAndFormParameters() { void TestFormFileAndJson(IFormFile value1, Todo value2) { } void TestFormFilesAndJson(IFormFile value1, IFormFile value2, Todo value3) { } void TestFormFileCollectionAndJson(IFormFileCollection value1, Todo value2) { } void TestFormFileAndJsonWithAttribute(IFormFile value1, [FromBody] int value2) { } void TestFormCollectionAndJson(IFormCollection value1, Todo value2) { } void TestFormWithAttributeAndJson([FromForm] string value1, Todo value2) { } void TestJsonAndFormFile(Todo value1, IFormFile value2) { } void TestJsonAndFormFiles(Todo value1, IFormFile value2, IFormFile value3) { } void TestJsonAndFormFileCollection(Todo value1, IFormFileCollection value2) { } void TestJsonAndFormFileWithAttribute(Todo value1, [FromForm] IFormFile value2) { } void TestJsonAndFormCollection(Todo value1, IFormCollection value2) { } void TestJsonAndFormWithAttribute(Todo value1, [FromForm] string value2) { } Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileAndJson)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFilesAndJson)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileAndJsonWithAttribute)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileCollectionAndJson)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormCollectionAndJson)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormWithAttributeAndJson)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFile)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFiles)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFileCollection)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFileWithAttribute)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormCollection)); Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormWithAttribute)); } [Fact] public void CreateThrowsNotSupportedExceptionIfIFormFileCollectionHasMetadataParameterName() { IFormFileCollection? formFilesArgument = null; void TestAction([FromForm(Name = "foo")] IFormFileCollection formFiles) { formFilesArgument = formFiles; } var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction)); Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormFileCollection.", nse.Message); } private readonly struct TraceIdentifier { private TraceIdentifier(string id) { Id = id; } public string Id { get; } public static implicit operator string(TraceIdentifier value) => value.Id; public static ValueTask<TraceIdentifier> BindAsync(HttpContext context) { return ValueTask.FromResult(new TraceIdentifier(context.TraceIdentifier)); } } public static TheoryData<HttpContent, string> FormContent { get { var dataset = new TheoryData<HttpContent, string>(); var multipartFormData = new MultipartFormDataContent("some-boundary"); multipartFormData.Add(new StringContent("hello"), "message"); multipartFormData.Add(new StringContent("foo"), "name"); dataset.Add(multipartFormData, "multipart/form-data;boundary=some-boundary"); var urlEncondedForm = new FormUrlEncodedContent(new Dictionary<string, string> { ["message"] = "hello", ["name"] = "foo" }); dataset.Add(urlEncondedForm, "application/x-www-form-urlencoded"); return dataset; } } [Fact] public void CreateThrowsNotSupportedExceptionIfIFormCollectionHasMetadataParameterName() { IFormCollection? formArgument = null; void TestAction([FromForm(Name = "foo")] IFormCollection formCollection) { formArgument = formCollection; } var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction)); Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message); } public static object[][] NullableFromParameterListActions { get { void TestParameterListRecordStruct([AsParameters] ParameterListRecordStruct? args) { } void TestParameterListRecordClass([AsParameters] ParameterListRecordClass? args) { } void TestParameterListStruct([AsParameters] ParameterListStruct? args) { } void TestParameterListClass([AsParameters] ParameterListClass? args) { } return new[] { new object[] { (Action<ParameterListRecordStruct?>)TestParameterListRecordStruct }, new object[] { (Action<ParameterListRecordClass?>)TestParameterListRecordClass }, new object[] { (Action<ParameterListStruct?>)TestParameterListStruct }, new object[] { (Action<ParameterListClass?>)TestParameterListClass }, }; } } [Theory] [MemberData(nameof(NullableFromParameterListActions))] public void RequestDelegateThrowsWhenNullableParameterList(Delegate action) { var parameter = action.Method.GetParameters()[0]; var httpContext = CreateHttpContext(); var exception = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(action)); Assert.Contains($"The nullable type '{TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)}' is not supported, mark the parameter as non-nullable.", exception.Message); } [Fact] public void RequestDelegateThrowsWhenParameterNameConflicts() { void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] SampleParameterList args2) { context.Items.Add("foo", args.Foo); } var httpContext = CreateHttpContext(); var exception = Assert.Throws<ArgumentException>(() => RequestDelegateFactory.Create(TestAction)); Assert.Contains("An item with the same key has already been added. Key: Foo", exception.Message); } private class ParameterListWithReadOnlyProperties { public ParameterListWithReadOnlyProperties() { ReadOnlyValue = 1; } public int Value { get; set; } public int ConstantValue => 1; public int ReadOnlyValue { get; } } string GetString(string name) { return $"Hello, {name}!"; } [Fact] public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithNullTargetFactory() { // Arrange var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( nameof(GetString), BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(string) }); var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["name"] = "TestName" }); // Act var factoryResult = RequestDelegateFactory.Create(methodInfo!, null, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); // Assert Assert.Equal(200, httpContext.Response.StatusCode); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal("Hello, TestName!", decodedResponseBody); } [Fact] public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithProvidedTargetFactory() { // Arrange var invoked = false; var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( nameof(GetString), BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(string) }); var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["name"] = "TestName" }); // Act Func<HttpContext, object> targetFactory = (context) => { invoked = true; context.Items["invoked"] = true; return Activator.CreateInstance(methodInfo!.DeclaringType!)!; }; var factoryResult = RequestDelegateFactory.Create(methodInfo!, targetFactory, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); // Assert Assert.Equal(200, httpContext.Response.StatusCode); Assert.True(invoked); var invokedInContext = Assert.IsType<bool>(httpContext.Items["invoked"]); Assert.True(invokedInContext); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal("Hello, TestName!", decodedResponseBody); } public static object[][] FSharpAsyncOfTMethods { get { FSharp.Control.FSharpAsync<string> FSharpAsyncOfTMethod() { return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("foo"); } FSharp.Control.FSharpAsync<string> FSharpAsyncOfTWithYieldMethod() { return FSharp.Control.FSharpAsync.AwaitTask(Yield()); async Task<string> Yield() { await Task.Yield(); return "foo"; } } FSharp.Control.FSharpAsync<object> FSharpAsyncOfObjectWithYieldMethod() { return FSharp.Control.FSharpAsync.AwaitTask(Yield()); async Task<object> Yield() { await Task.Yield(); return "foo"; } } return new object[][] { new object[] { (Func<FSharp.Control.FSharpAsync<string>>)FSharpAsyncOfTMethod }, new object[] { (Func<FSharp.Control.FSharpAsync<string>>)FSharpAsyncOfTWithYieldMethod }, new object[] { (Func<FSharp.Control.FSharpAsync<object>>)FSharpAsyncOfObjectWithYieldMethod } }; } } [Theory] [MemberData(nameof(FSharpAsyncOfTMethods))] public async Task CanInvokeFilter_OnFSharpAsyncOfTReturningHandler(Delegate @delegate) { // Arrange var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(); httpContext.Response.Body = responseBodyStream; // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal("foo", decodedResponseBody); } public static object[][] VoidReturningMethods { get { void VoidMethod() { } ValueTask ValueTaskMethod() { return ValueTask.CompletedTask; } ValueTask<FSharp.Core.Unit> ValueTaskOfUnitMethod() { return ValueTask.FromResult(default(FSharp.Core.Unit)!); } Task TaskMethod() { return Task.CompletedTask; } Task<FSharp.Core.Unit> TaskOfUnitMethod() { return Task.FromResult(default(FSharp.Core.Unit)!); } FSharp.Control.FSharpAsync<FSharp.Core.Unit> FSharpAsyncOfUnitMethod() { return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(FSharp.Core.Unit)!); } async ValueTask ValueTaskWithYieldMethod() { await Task.Yield(); } async Task TaskWithYieldMethod() { await Task.Yield(); } FSharp.Control.FSharpAsync<FSharp.Core.Unit> FSharpAsyncOfUnitWithYieldMethod() { return FSharp.Control.FSharpAsync.AwaitTask(Yield()); async Task<FSharp.Core.Unit> Yield() { await Task.Yield(); return default!; } } return new object[][] { new object[] { (Action)VoidMethod }, new object[] { (Func<ValueTask>)ValueTaskMethod }, new object[] { (Func<ValueTask<FSharp.Core.Unit>>)ValueTaskOfUnitMethod }, new object[] { (Func<Task>)TaskMethod }, new object[] { (Func<Task<FSharp.Core.Unit>>)TaskOfUnitMethod }, new object[] { (Func<FSharp.Control.FSharpAsync<FSharp.Core.Unit>>)FSharpAsyncOfUnitMethod }, new object[] { (Func<ValueTask>)ValueTaskWithYieldMethod }, new object[] { (Func<Task>)TaskWithYieldMethod}, new object[] { (Func<FSharp.Control.FSharpAsync<FSharp.Core.Unit>>)FSharpAsyncOfUnitWithYieldMethod } }; } } [Theory] [MemberData(nameof(VoidReturningMethods))] public async Task CanInvokeFilter_OnVoidReturningHandler(Delegate @delegate) { // Arrange var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(); httpContext.Response.Body = responseBodyStream; // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(String.Empty, decodedResponseBody); } [Fact] public async Task CanInvokeFilter_OnTaskModifyingHttpContext() { // Arrange var tcs = new TaskCompletionSource(); async Task HandlerWithTaskAwait(HttpContext c) { await tcs.Task; await Task.Yield(); c.Response.StatusCode = 400; }; var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(); httpContext.Response.Body = responseBodyStream; // Act var factoryResult = RequestDelegateFactory.Create(HandlerWithTaskAwait, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; var request = requestDelegate(httpContext); tcs.TrySetResult(); await request; Assert.Equal(400, httpContext.Response.StatusCode); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); Assert.Equal(string.Empty, decodedResponseBody); } public static object[][] TasksOfTypesMethods { get { ValueTask<TodoStruct> ValueTaskOfStructMethod() { return ValueTask.FromResult(new TodoStruct { Name = "Test todo" }); } async ValueTask<TodoStruct> ValueTaskOfStructWithYieldMethod() { await Task.Yield(); return new TodoStruct { Name = "Test todo" }; } Task<TodoStruct> TaskOfStructMethod() { return Task.FromResult(new TodoStruct { Name = "Test todo" }); } async Task<TodoStruct> TaskOfStructWithYieldMethod() { await Task.Yield(); return new TodoStruct { Name = "Test todo" }; } FSharp.Control.FSharpAsync<TodoStruct> FSharpAsyncOfStructMethod() { return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new TodoStruct { Name = "Test todo" }); } FSharp.Control.FSharpAsync<TodoStruct> FSharpAsyncOfStructWithYieldMethod() { return FSharp.Control.FSharpAsync.AwaitTask(Yield()); async Task<TodoStruct> Yield() { await Task.Yield(); return new TodoStruct { Name = "Test todo" }; } } return new object[][] { new object[] { (Func<ValueTask<TodoStruct>>)ValueTaskOfStructMethod }, new object[] { (Func<ValueTask<TodoStruct>>)ValueTaskOfStructWithYieldMethod }, new object[] { (Func<Task<TodoStruct>>)TaskOfStructMethod }, new object[] { (Func<Task<TodoStruct>>)TaskOfStructWithYieldMethod }, new object[] { (Func<FSharp.Control.FSharpAsync<TodoStruct>>)FSharpAsyncOfStructMethod }, new object[] { (Func<FSharp.Control.FSharpAsync<TodoStruct>>)FSharpAsyncOfStructWithYieldMethod } }; } } [Theory] [MemberData(nameof(TasksOfTypesMethods))] public async Task CanInvokeFilter_OnHandlerReturningTasksOfStruct(Delegate @delegate) { // Arrange var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(); httpContext.Response.Body = responseBodyStream; // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { return await next(context); } }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); Assert.Equal(200, httpContext.Response.StatusCode); var deserializedResponseBody = JsonSerializer.Deserialize<TodoStruct>(responseBodyStream.ToArray(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); Assert.Equal("Test todo", deserializedResponseBody.Name); } [Fact] public void Create_DoesNotAddDelegateMethodInfo_AsMetadata() { // Arrange var @delegate = () => { }; // Act var result = RequestDelegateFactory.Create(@delegate); // Assert // RouteHandlerEndpointDataSource adds the MethodInfo as the first item in RouteHandlerOptions.EndointMetadata Assert.Empty(result.EndpointMetadata); } [Fact] public void Create_AddJsonResponseType_AsMetadata() { var @delegate = () => new object(); var result = RequestDelegateFactory.Create(@delegate); var responseMetadata = Assert.Single(result.EndpointMetadata.OfType<IProducesResponseTypeMetadata>()); Assert.Equal("application/json", Assert.Single(responseMetadata.ContentTypes)); Assert.Equal(typeof(object), responseMetadata.Type); } [Fact] public void Create_AddPlaintextResponseType_AsMetadata() { var @delegate = () => "Hello"; var result = RequestDelegateFactory.Create(@delegate); var responseMetadata = Assert.Single(result.EndpointMetadata.OfType<IProducesResponseTypeMetadata>()); Assert.Equal("text/plain", Assert.Single(responseMetadata.ContentTypes)); Assert.Equal(typeof(string), responseMetadata.Type); } [Fact] public void Create_DoesNotAddAnythingBefore_ThePassedInEndpointMetadata() { // Arrange var @delegate = (AddsCustomParameterMetadataBindable param1) => { }; var customMetadata = new CustomEndpointMetadata(); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { customMetadata }) }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert // RouteHandlerEndpointDataSource adds things like the MethodInfo, HttpMethodMetadata and attributes to RouteHandlerOptions.EndointMetadata, // but we just specified our CustomEndpointMetadata in this test. Assert.Collection(result.EndpointMetadata, m => Assert.Same(customMetadata, m), m => Assert.True(m is IParameterBindingMetadata { HasBindAsync : true }), m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter })); } [Fact] public void Create_DoesNotAddDelegateAttributes_AsMetadata() { // Arrange var @delegate = [Attribute1, Attribute2] () => { }; // Act var result = RequestDelegateFactory.Create(@delegate); // Assert // RouteHandlerEndpointDataSource adds the attributes to RouteHandlerOptions.EndointMetadata Assert.Empty(result.EndpointMetadata); } [Fact] public void Create_DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() { // Arrange var @delegate = (AddsCustomParameterMetadataBindable param1, AddsCustomParameterMetadata param2) => { }; // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param2" }); } [Fact] public void Create_DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (AddsCustomParameterMetadata param1) => { }; // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); } [Fact] public void Create_DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => new AddsCustomEndpointMetadataResult(); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); } [Fact] public void Create_DiscoversEndpointMetadata_FromTaskWrappedReturnTypeImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => Task.FromResult(new AddsCustomEndpointMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); } [Fact] public void Create_DiscoversEndpointMetadata_FromValueTaskWrappedReturnTypeImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => ValueTask.FromResult(new AddsCustomEndpointMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); } [Fact] public void Create_DiscoversEndpointMetadata_FromFSharpAsyncWrappedReturnTypeImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new AddsCustomEndpointMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.DoesNotContain(result.EndpointMetadata, m => m is IProducesResponseTypeMetadata); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromTaskWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => Task.FromResult(new CountsDefaultEndpointMetadataResult()); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.DoesNotContain(result.EndpointMetadata, m => m is IProducesResponseTypeMetadata); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromValueTaskWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => ValueTask.FromResult(new CountsDefaultEndpointMetadataResult()); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.DoesNotContain(result.EndpointMetadata, m => m is IProducesResponseTypeMetadata); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromFSharpAsyncWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = () => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CountsDefaultEndpointMetadataResult()); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.DoesNotContain(result.EndpointMetadata, m => m is IProducesResponseTypeMetadata); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider() { // Arrange var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); } [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); } [Fact] public void Create_CombinesAllMetadata_InCorrectOrder() { // Arrange var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataPoco(); var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List<object> { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Collection(result.EndpointMetadata, // Initial metadata from RequestDelegateFactoryOptions.EndpointBuilder. If the caller want to override inferred metadata, // They need to call InferMetadata first, then add the overriding metadata, and then call Create with InferMetadata's result. // This is demonstrated in the following tests. m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller }), // Inferred AcceptsMetadata from RDF for complex type m => Assert.True(m is AcceptsMetadata am && am.RequestType == typeof(AddsCustomParameterMetadata)), // Inferred ParameterBinding metadata m => Assert.True(m is IParameterBindingMetadata { Name: "param1" }), // Inferred IDisableCookieRedirectMetadata from RDF for complex request and response type m => Assert.True(m is IDisableCookieRedirectMetadata), // Inferred ProducesResponseTypeMetadata from RDF for complex type m => Assert.Equal(typeof(CountsDefaultEndpointMetadataPoco), ((IProducesResponseTypeMetadata)m).Type), // Metadata provided by parameters implementing IEndpointParameterMetadataProvider m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), // Metadata provided by parameters implementing IEndpointMetadataProvider m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter }), // Metadata provided by return type implementing IEndpointMetadataProvider m => Assert.True(m is MetadataCountMetadata { Count: 7 })); } [Fact] public void Create_FlowsRoutePattern_ToMetadataProvider() { // Arrange var @delegate = (AddsRoutePatternMetadata param1) => { }; var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/test/pattern"), order: 0); var options = new RequestDelegateFactoryOptions { EndpointBuilder = builder, }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.Contains(result.EndpointMetadata, m => m is RoutePatternMetadata { RoutePattern: "/test/pattern" }); } [Fact] public void Create_DoesNotInferMetadata_GivenManuallyConstructedMetadataResult() { var invokeCount = 0; // Arrange var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => { invokeCount++; return new CountsDefaultEndpointMetadataResult(); }; var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(), }; var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() }; var httpContext = CreateHttpContext(); // An empty object should deserialize to AddsCustomParameterMetadata since it has no required properties. var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new object()); var stream = new MemoryStream(requestBodyBytes); httpContext.Request.Body = stream; httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true)); // Act var result = RequestDelegateFactory.Create(@delegate, options, metadataResult); // Assert Assert.Contains(result.EndpointMetadata, m => m is IParameterBindingMetadata { Name: "param1" }); Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata); // Make extra sure things are running as expected, as this non-InferMetadata path is no longer exercised by RouteEndpointDataSource, // and most of the other unit tests don't pass in a metadataResult without a cached factory context. Assert.True(result.RequestDelegate(httpContext).IsCompletedSuccessfully); Assert.Equal(1, invokeCount); } [Fact] public void InferMetadata_PopulatesAcceptsMetadata_WhenReadFromForm() { // Arrange var @delegate = void (IFormCollection formCollection) => { }; var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(), }; // Act var metadataResult = RequestDelegateFactory.InferMetadata(@delegate.Method, options); // Assert var allAcceptsMetadata = metadataResult.EndpointMetadata.OfType<IAcceptsMetadata>(); var acceptsMetadata = Assert.Single(allAcceptsMetadata); Assert.NotNull(acceptsMetadata); Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes); } [Fact] public void InferMetadata_PopulatesCachedContext() { // Arrange var @delegate = void () => { }; var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(), }; // Act var metadataResult = RequestDelegateFactory.InferMetadata(@delegate.Method, options); // Assert Assert.NotNull(metadataResult.CachedFactoryContext); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (Todo todo) => new RemovesAcceptsMetadataResult(); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByTaskWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (Todo todo) => Task.FromResult(new RemovesAcceptsMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByValueTaskWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (Todo todo) => ValueTask.FromResult(new RemovesAcceptsMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByFSharpAsyncWrappedReturnTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (Todo todo) => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new RemovesAcceptsMetadataResult()); // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() { // Arrange var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello"; // Act var result = RequestDelegateFactory.Create(@delegate); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider() { // Arrange var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions(); // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_SetsApplicationServices_OnEndpointMetadataContext() { // Arrange var @delegate = (Todo todo) => new AccessesServicesMetadataResult(); var metadataService = new MetadataService(); var serviceProvider = new ServiceCollection().AddSingleton(metadataService).BuildServiceProvider(); // Act var result = RequestDelegateFactory.Create(@delegate, new() { ServiceProvider = serviceProvider }); // Assert Assert.Contains(result.EndpointMetadata, m => m is MetadataService); } [Fact] public void Create_SetsApplicationServices_OnEndpointParameterMetadataContext() { // Arrange var @delegate = (AccessesServicesMetadataBinder parameter1) => "Test"; var metadataService = new MetadataService(); var serviceProvider = new ServiceCollection().AddSingleton(metadataService).BuildServiceProvider(); // Act var result = RequestDelegateFactory.Create(@delegate, new() { ServiceProvider = serviceProvider }); // Assert Assert.Contains(result.EndpointMetadata, m => m is MetadataService); } [Fact] public void Create_ReturnsSameRequestDelegatePassedIn_IfNotModifiedByFilters() { RequestDelegate initialRequestDelegate = static (context) => Task.CompletedTask; var invokeCount = 0; RequestDelegateFactoryOptions options = new() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => { invokeCount++; return next; }, (routeHandlerContext, next) => { invokeCount++; return next; }, }), }; var result = RequestDelegateFactory.Create(initialRequestDelegate, options); Assert.Same(initialRequestDelegate, result.RequestDelegate); Assert.Equal(2, invokeCount); } [Fact] public void Create_Populates_EndpointBuilderWithRequestDelegateAndMetadata() { RequestDelegate requestDelegate = static context => Task.CompletedTask; RequestDelegateFactoryOptions options = new() { EndpointBuilder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), order: 0), }; var result = RequestDelegateFactory.Create(requestDelegate, options); Assert.Same(options.EndpointBuilder.RequestDelegate, result.RequestDelegate); Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata); } [Fact] public async Task RDF_CanAssertOnEmptyResult() { var @delegate = (string name, HttpContext context) => context.Items.Add("param", name); var result = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>() { (routeHandlerContext, next) => async (context) => { var response = await next(context); Assert.IsType<EmptyHttpResult>(response); Assert.Same(Results.Empty, response); return response; } }), }); var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> { ["name"] = "Tester" }); await result.RequestDelegate(httpContext); } private class ParameterListRequiredNullableStringFromDifferentSources { public HttpContext? HttpContext { get; set; } [FromRoute] public required StringValues? RequiredRouteParam { get; set; } [FromQuery] public required StringValues? RequiredQueryParam { get; set; } [FromHeader] public required StringValues? RequiredHeaderParam { get; set; } } [Fact] public async Task RequestDelegateFactory_AsParameters_SupportsNullableRequiredMember() { // Arrange static void TestAction([AsParameters] ParameterListRequiredNullableStringFromDifferentSources args) { args.HttpContext!.Items.Add("RequiredRouteParam", args.RequiredRouteParam); args.HttpContext!.Items.Add("RequiredQueryParam", args.RequiredQueryParam); args.HttpContext!.Items.Add("RequiredHeaderParam", args.RequiredHeaderParam); } var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; // Act await requestDelegate(httpContext); // Assert that when properties are required but nullable // we evaluate them as optional because required members // must be initialized but they can be initialized to null // when an NRT is required. Assert.Equal(200, httpContext.Response.StatusCode); Assert.Null(httpContext.Items["RequiredRouteParam"]); Assert.Null(httpContext.Items["RequiredQueryParam"]); Assert.Null(httpContext.Items["RequiredHeaderParam"]); } private class ParameterListFromHeaderCommaSeparatedValues { [FromHeader(Name = "q")] public required StringValues? BoundToStringValues { get; set; } [FromHeader(Name = "q")] public string? BoundToString { get; set; } [FromHeader(Name = "q")] public string[]? BoundToStringArray { get; set; } [FromHeader(Name = "q")] public int[]? BoundToIntArray { get; set; } } [Theory] [InlineData("", new string[] { }, new int[] {})] [InlineData(" ", new string[] { }, new int[] { })] [InlineData(",", new string[] { }, new int[] { })] [InlineData("100", new string[] { "100" }, new int[] { 100 })] [InlineData("1,2", new string[] { "1", "2" }, new int[] { 1, 2 })] [InlineData("1, 2 , 3", new string[] { "1", "2", "3" }, new int[] { 1, 2, 3 })] public async Task RequestDelegateFactory_FromHeader_CommaSeparatedValues(string headerValue, string[] expectedStringArray, int[] expectedIntArray) { // Arrange var httpContext = CreateHttpContext(); httpContext.Request.Headers["q"] = headerValue; void TestAction([AsParameters] ParameterListFromHeaderCommaSeparatedValues args) { httpContext.Items[nameof(args.BoundToStringValues)] = args.BoundToStringValues; httpContext.Items[nameof(args.BoundToString)] = args.BoundToString; httpContext.Items[nameof(args.BoundToStringArray)] = args.BoundToStringArray; httpContext.Items[nameof(args.BoundToIntArray)] = args.BoundToIntArray; } var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; // Act await requestDelegate(httpContext); // Assert Assert.Equal(headerValue, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToString)]); Assert.Equal(new StringValues(headerValue), httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringValues)]); Assert.Equal(expectedStringArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringArray)]); Assert.Equal(expectedIntArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToIntArray)]); } #nullable disable private class ParameterListMixedRequiredStringsFromDifferentSources { public HttpContext HttpContext { get; set; } [FromRoute] public required string RequiredRouteParam { get; set; } [FromRoute] public string OptionalRouteParam { get; set; } [FromQuery] public required string RequiredQueryParam { get; set; } [FromQuery] public string OptionalQueryParam { get; set; } [FromHeader] public required string RequiredHeaderParam { get; set; } [FromHeader] public string OptionalHeaderParam { get; set; } } [Fact] public async Task RequestDelegateFactory_AsParameters_SupportsRequiredMember_NullabilityDisabled() { // Arange static void TestAction([AsParameters] ParameterListMixedRequiredStringsFromDifferentSources args) { } var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; // Act await requestDelegate(httpContext); // Assert that we only execute required parameter // checks for members that have the required modifier Assert.Equal(400, httpContext.Response.StatusCode); var logs = TestSink.Writes.ToArray(); Assert.Equal(3, logs.Length); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); Assert.Equal(LogLevel.Debug, logs[0].LogLevel); Assert.Equal(@"Required parameter ""string RequiredRouteParam"" was not provided from route.", logs[0].Message); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); Assert.Equal(LogLevel.Debug, logs[1].LogLevel); Assert.Equal(@"Required parameter ""string RequiredQueryParam"" was not provided from query string.", logs[1].Message); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[2].EventId); Assert.Equal(LogLevel.Debug, logs[2].LogLevel); Assert.Equal(@"Required parameter ""string RequiredHeaderParam"" was not provided from header.", logs[2].Message); } #nullable enable [ConditionalFact] [RemoteExecutionSupported] public void RequestDelegateFactory_WhenJsonIsReflectionEnabledByDefaultFalse() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", false.ToString()); using var remoteHandle = RemoteExecutor.Invoke(static () => { // Arrange var @delegate = (string task) => new Todo(); // IsReflectionEnabledByDefault defaults to `false` when `PublishTrimmed=true`. For these scenarios, we // expect users to configure JSON source generation as instructed in the `NotSupportedException` message. var exception = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(@delegate)); Assert.Contains("Microsoft.AspNetCore.Routing.Internal.RequestDelegateFactoryTests+Todo", exception.Message); Assert.Contains("JsonSerializableAttribute", exception.Message); }, options); } [ConditionalFact] [RemoteExecutionSupported] public void RequestDelegateFactory_WhenJsonIsReflectionEnabledByDefaultTrue() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", true.ToString()); using var remoteHandle = RemoteExecutor.Invoke(static () => { // Arrange var @delegate = (string task) => new Todo(); // Assert var exception = Record.Exception(() => RequestDelegateFactory.Create(@delegate)); Assert.Null(exception); }, options); } private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); return new() { RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), Features = { [typeof(IHttpResponseFeature)] = responseFeature, [typeof(IHttpResponseBodyFeature)] = responseFeature, [typeof(IHttpRequestLifetimeFeature)] = new TestHttpRequestLifetimeFeature(), } }; } private EndpointBuilder CreateEndpointBuilder(IEnumerable<object>? metadata = null) { var builder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), 0); if (metadata is not null) { ((List<object>)builder.Metadata).AddRange(metadata); } return builder; } private EndpointBuilder CreateEndpointBuilderFromFilterFactories(IEnumerable<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>> filterFactories) { var builder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), 0); ((List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>)builder.FilterFactories).AddRange(filterFactories); return builder; } private record MetadataService; private class AccessesServicesMetadataResult : IResult, IEndpointMetadataProvider { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { if (builder.ApplicationServices.GetRequiredService<MetadataService>() is { } metadataService) { builder.Metadata.Add(metadataService); } } public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; } private class AccessesServicesMetadataBinder : IEndpointMetadataProvider { public static ValueTask<AccessesServicesMetadataBinder> BindAsync(HttpContext context, ParameterInfo parameter) => new(new AccessesServicesMetadataBinder()); public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { if (builder.ApplicationServices.GetRequiredService<MetadataService>() is { } metadataService) { builder.Metadata.Add(metadataService); } } } private class Attribute1 : Attribute { } private class Attribute2 : Attribute { } private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IResult { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } private class AddsNoEndpointMetadataResult : IEndpointMetadataProvider, IResult { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, IResult { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { var currentMetadataCount = builder.Metadata.Count; builder.Metadata.Add(new MetadataCountMetadata { Count = currentMetadataCount }); } public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; } private class CountsDefaultEndpointMetadataPoco : IEndpointMetadataProvider { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { var currentMetadataCount = builder.Metadata.Count; builder.Metadata.Add(new MetadataCountMetadata { Count = currentMetadataCount }); } } private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider { public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { if (builder.Metadata is not null) { for (int i = builder.Metadata.Count - 1; i >= 0; i--) { var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { builder.Metadata.RemoveAt(i); } } } } } private class RemovesAcceptsMetadata : IEndpointMetadataProvider { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { if (builder.Metadata is not null) { for (int i = builder.Metadata.Count - 1; i >= 0; i--) { var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { builder.Metadata.RemoveAt(i); } } } } } private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { if (builder.Metadata is not null) { for (int i = builder.Metadata.Count - 1; i >= 0; i--) { var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { builder.Metadata.RemoveAt(i); } } } } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } private class AddsCustomParameterMetadataAsProperty : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Property }); } } // TODO: Binding breaks if we explicitly implement IParsable. :( // We could special-case IParsable because we have a reference to it. The check for `!method.IsAbstract` in GetStaticMethodFromHierarchy // stops us from finding it now. But even if we did find it, we haven't implemented the correct code gen to call it for unreferenced interfaces. // We might have to use Type.GetInterfaceMap. See previous discussion: https://github.com/dotnet/aspnetcore/pull/40926#discussion_r837781209 // // System.InvalidOperationException : TryParse method found on AddsCustomParameterMetadata with incorrect format. Must be a static method with format // bool TryParse(string, IFormatProvider, out AddsCustomParameterMetadata) // bool TryParse(string, out AddsCustomParameterMetadata) // but found // static Boolean TryParse(System.String, System.IFormatProvider, AddsCustomParameterMetadata ByRef) private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider//, IParsable<AddsCustomParameterMetadata> { public AddsCustomParameterMetadataAsProperty? Data { get; set; } public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } //static bool IParsable<AddsCustomParameterMetadata>.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out AddsCustomParameterMetadata result) //{ // result = new(); // return true; //} //static AddsCustomParameterMetadata IParsable<AddsCustomParameterMetadata>.Parse(string s, IFormatProvider? provider) => throw new NotSupportedException(); } private class AddsCustomParameterMetadataBindable : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { public static ValueTask<AddsCustomParameterMetadataBindable> BindAsync(HttpContext context, ParameterInfo parameter) => default; public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } } private class AddsRoutePatternMetadata : IEndpointMetadataProvider { public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { if (builder is not RouteEndpointBuilder reb) { return; } builder.Metadata.Add(new RoutePatternMetadata { RoutePattern = reb.RoutePattern?.RawText ?? string.Empty }); } } private class MetadataCountMetadata { public int Count { get; init; } } private class ParameterNameMetadata { public string? Name { get; init; } } private class CustomEndpointMetadata { public string? Data { get; init; } public MetadataSource Source { get; init; } } private class RoutePatternMetadata { public string RoutePattern { get; init; } = String.Empty; } private enum MetadataSource { Caller, Parameter, ReturnType, Property } private class Todo : ITodo { public int Id { get; set; } public string? Name { get; set; } = "Todo"; public bool IsComplete { get; set; } } private class JsonTodoChild : JsonTodo { public string? Child { get; set; } } [JsonPolymorphic] [JsonDerivedType(typeof(JsonTodoChild), nameof(JsonTodoChild))] private class JsonTodo : Todo { public static async ValueTask<JsonTodo?> BindAsync(HttpContext context, ParameterInfo parameter) { // manually call deserialize so we don't check content type var body = await JsonSerializer.DeserializeAsync<JsonTodo>(context.Request.Body); context.Request.Body.Position = 0; return body; } } private record struct TodoStruct(int Id, string? Name, bool IsComplete) : ITodo; private class FromRouteAttribute : Attribute, IFromRouteMetadata { public string? Name { get; set; } } private class FromQueryAttribute : Attribute, IFromQueryMetadata { public string? Name { get; set; } } private class FromHeaderAttribute : Attribute, IFromHeaderMetadata { public string? Name { get; set; } } private class FromBodyAttribute : Attribute, IFromBodyMetadata { public bool AllowEmpty { get; set; } } private class FromFormAttribute : Attribute, IFromFormMetadata { public string? Name { get; set; } } private class FromServiceAttribute : Attribute, IFromServiceMetadata { } class HttpHandler { private int _calls; public void Handle(HttpContext httpContext) { _calls++; httpContext.Items["calls"] = _calls; } } private interface IMyService { } private class MyService : IMyService { } private class CustomResult : IResult { private readonly string _resultString; public CustomResult(string resultString) { _resultString = resultString; } public Task ExecuteAsync(HttpContext httpContext) { return httpContext.Response.WriteAsync(_resultString); } } private struct StructResult : IResult { private readonly string _resultString; public StructResult(string resultString) { _resultString = resultString; } public Task ExecuteAsync(HttpContext httpContext) { return httpContext.Response.WriteAsync(_resultString); } } private class ExceptionThrowingRequestBodyStream : Stream { private readonly Exception _exceptionToThrow; public ExceptionThrowingRequestBodyStream(Exception exceptionToThrow) { _exceptionToThrow = exceptionToThrow; } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotImplementedException(); public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override void Flush() { throw new NotImplementedException(); } public override int Read(byte[] buffer, int offset, int count) { throw _exceptionToThrow; } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } } private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory { public IServiceProvider ServiceProvider => this; public IServiceScope CreateScope() { return new EmptyServiceProvider(); } public void Dispose() { } public object? GetService(Type serviceType) { if (serviceType == typeof(IServiceScopeFactory)) { return this; } return null; } } private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature { private readonly CancellationTokenSource _requestAbortedCts = new(); public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); } public void Abort() { _requestAbortedCts.Cancel(); } } private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature { public int StatusCode { get; set; } = 200; public string? ReasonPhrase { get; set; } public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public bool HasStarted { get; private set; } // Assume any access to the response Body/Stream/Writer is writing for test purposes. public Stream Body { get { HasStarted = true; return Stream.Null; } set { } } public Stream Stream { get { HasStarted = true; return Stream.Null; } } public PipeWriter Writer { get { HasStarted = true; return PipeWriter.Create(Stream.Null); } } public Task StartAsync(CancellationToken cancellationToken = default) { HasStarted = true; return Task.CompletedTask; } public Task CompleteAsync() { HasStarted = true; return Task.CompletedTask; } public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) { HasStarted = true; return Task.CompletedTask; } public void DisableBuffering() { } public void OnStarting(Func<object, Task> callback, object state) { } public void OnCompleted(Func<object, Task> callback, object state) { } } private class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature { public RequestBodyDetectionFeature(bool canHaveBody) { CanHaveBody = canHaveBody; } public bool CanHaveBody { get; } } private class TlsConnectionFeature : ITlsConnectionFeature { public TlsConnectionFeature(X509Certificate2 clientCertificate) { ClientCertificate = clientCertificate; } public X509Certificate2? ClientCertificate { get; set; } public Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); } } } internal static class TestExtensionResults { public static IResult TestResult(this IResultExtensions resultExtensions, string name) { return Results.Ok(FormattableString.Invariant($"Hello {name}. This is from an extension method.")); } }