diff --git a/AspNetCore.sln b/AspNetCore.sln index 4f798d3ca2d3..168a880c6d1e 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1572,12 +1572,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BrowserTesting", "BrowserTe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.BrowserTesting", "src\Shared\BrowserTesting\src\Microsoft.AspNetCore.BrowserTesting.csproj", "{B739074E-6652-4F5B-B37E-775DC2245FEC}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{722E5A66-D84A-4689-AA87-7197FF5D7070}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmLinkerTest", "src\Components\WebAssembly\testassets\WasmLinkerTest\WasmLinkerTest.csproj", "{3B375FFC-1E38-453E-A26D-A510CCD3339E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\Routing\samples\MapActionSample\MapActionSample.csproj", "{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{71287382-95EF-490D-A285-87196E29E88A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HostedBlazorWebassemblyApp", "HostedBlazorWebassemblyApp", "{B4226BE2-DCB7-40C5-93F2-94C9BD6F4394}" @@ -1618,6 +1614,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWinFormsApp", "src\Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks", "src\Http\Http.Abstractions\perf\Microbenchmarks\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "{3F752B48-2936-4FCA-B0DC-4AB0F788F897}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\samples\MapActionSample\MapActionSample.csproj", "{A661D867-708A-494E-8B6B-6558804F9A3F}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F0849E7E-61DB-4849-9368-9E7BC125DCB0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsTestApp", "src\Components\WebView\Platforms\WindowsForms\testassets\WinFormsTestApp\WinFormsTestApp.csproj", "{99EE7769-3C81-477B-B947-0A5CBCD5B27D}" @@ -7521,18 +7519,6 @@ Global {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.Build.0 = Release|Any CPU {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.ActiveCfg = Release|Any CPU {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.Build.0 = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.Build.0 = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.ActiveCfg = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.Build.0 = Debug|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.Build.0 = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.ActiveCfg = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.Build.0 = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.ActiveCfg = Release|Any CPU - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.Build.0 = Release|Any CPU {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -7665,6 +7651,18 @@ Global {3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x64.Build.0 = Release|Any CPU {3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.ActiveCfg = Release|Any CPU {3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.Build.0 = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.ActiveCfg = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.Build.0 = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.Build.0 = Debug|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.Build.0 = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.ActiveCfg = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.Build.0 = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.ActiveCfg = Release|Any CPU + {A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.Build.0 = Release|Any CPU {99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.Build.0 = Debug|Any CPU {99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -8489,9 +8487,7 @@ Global {22EA0993-8DFC-40C2-8481-8E85E21EFB56} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} {8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034} {B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D} - {722E5A66-D84A-4689-AA87-7197FF5D7070} = {54C42F57-5447-4C21-9812-4AF665567566} {3B375FFC-1E38-453E-A26D-A510CCD3339E} = {7D2B0799-A634-42AC-AE77-5D167BA51389} - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070} {71287382-95EF-490D-A285-87196E29E88A} = {562D5067-8CD8-4F19-BCBB-873204932C61} {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} = {71287382-95EF-490D-A285-87196E29E88A} {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} @@ -8512,6 +8508,7 @@ Global {3BA297F8-1CA1-492D-AE64-A60B825D8501} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E} {CC740832-D268-47A3-9058-B9054F8397E2} = {D3B76F4E-A980-45BF-AEA1-EA3175B0B5A1} {3F752B48-2936-4FCA-B0DC-4AB0F788F897} = {DCBBDB52-4A49-4141-8F4D-81C0FFFB7BD5} + {A661D867-708A-494E-8B6B-6558804F9A3F} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1} {F0849E7E-61DB-4849-9368-9E7BC125DCB0} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E} {99EE7769-3C81-477B-B947-0A5CBCD5B27D} = {F0849E7E-61DB-4849-9368-9E7BC125DCB0} {94D0D6F3-8632-41DE-908B-47A787D570FF} = {5241CF68-66A0-4724-9BAA-36DB959A5B11} diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 948c77fd094c..d96cddbdfc69 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -2,9 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -23,28 +22,39 @@ namespace Microsoft.AspNetCore.Http /// public static class RequestDelegateFactory { - private static readonly MethodInfo ChangeTypeMethodInfo = GetMethodInfo>((value, type) => Convert.ChangeType(value, type, CultureInfo.InvariantCulture)); - private static readonly MethodInfo ExecuteTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteTaskOfStringMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskOfStringMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteTaskResultOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueResultTaskOfTMethodInfo = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo GetRequiredServiceMethodInfo = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; - private static readonly MethodInfo ResultWriteResponseAsync = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!; - private static readonly MethodInfo StringResultWriteResponseAsync = GetMethodInfo>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default)); - private static readonly MethodInfo JsonResultWriteResponseAsync = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); - private static readonly MemberInfo CompletedTaskMemberInfo = GetMemberInfo>(() => Task.CompletedTask); - - private static readonly ParameterExpression TargetArg = Expression.Parameter(typeof(object), "target"); - private static readonly ParameterExpression HttpContextParameter = Expression.Parameter(typeof(HttpContext), "httpContext"); - private static readonly ParameterExpression DeserializedBodyArg = Expression.Parameter(typeof(object), "bodyValue"); - - private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestServices)); - private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Request)); - private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Response)); - private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestAborted)); + private static readonly MethodInfo ExecuteTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; + private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!; + private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default)); + private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); + private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod(); + private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo>((httpContext, parameterType, parameterName, sourceValue) => + Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue)); + + private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target"); + private static readonly ParameterExpression HttpContextExpr = Expression.Parameter(typeof(HttpContext), "httpContext"); + private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue"); + private static readonly ParameterExpression WasTryParseFailureExpr = Expression.Variable(typeof(bool), "wasTryParseFailure"); + private static readonly ParameterExpression TempSourceStringExpr = Expression.Variable(typeof(string), "tempSourceString"); + + private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestServices)); + private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Request)); + private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Response)); + private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestAborted)); + private static readonly MemberExpression RouteValuesExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues)); + private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query)); + private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers)); + private static readonly MemberExpression FormExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); + private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, nameof(HttpResponse.StatusCode)); + private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo>(() => Task.CompletedTask)); + + private static readonly ConcurrentDictionary TryParseMethodCache = new(); /// /// Creates a implementation for . @@ -60,15 +70,15 @@ public static RequestDelegate Create(Delegate action) var targetExpression = action.Target switch { - object => Expression.Convert(TargetArg, action.Target.GetType()), + object => Expression.Convert(TargetExpr, action.Target.GetType()), null => null, }; - var untargetedRequestDelegate = CreateRequestDelegate(action.Method, targetExpression); + var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, targetExpression); return httpContext => { - return untargetedRequestDelegate(action.Target, httpContext); + return targetableRequestDelegate(action.Target, httpContext); }; } @@ -84,11 +94,11 @@ public static RequestDelegate Create(MethodInfo methodInfo) throw new ArgumentNullException(nameof(methodInfo)); } - var untargetedRequestDelegate = CreateRequestDelegate(methodInfo, targetExpression: null); + var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression: null); return httpContext => { - return untargetedRequestDelegate(null, httpContext); + return targetableRequestDelegate(null, httpContext); }; } @@ -115,16 +125,16 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func { - return untargetedRequestDelegate(targetFactory(httpContext), httpContext); + return targetableRequestDelegate(targetFactory(httpContext), httpContext); }; } - private static Func CreateRequestDelegate(MethodInfo methodInfo, Expression? targetExpression) + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression) { // Non void return type @@ -142,235 +152,319 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func(methodParameters.Length); + var responseWritingMethodCall = factoryContext.CheckForTryParseFailure ? + CreateTryParseCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments) : + CreateResponseWritingMethodCall(methodInfo, targetExpression, arguments); - foreach (var parameter in methodParameters) + return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); + } + + private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext) + { + if (parameters is null || parameters.Length == 0) { - Expression paramterExpression = Expression.Default(parameter.ParameterType); + return Array.Empty(); + } - var parameterCustomAttributes = parameter.GetCustomAttributes(); + var args = new Expression[parameters.Length]; - if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute) - { - var routeValuesProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues)); - paramterExpression = BindParamenter(routeValuesProperty, parameter, routeAttribute.Name); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) - { - var queryProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query)); - paramterExpression = BindParamenter(queryProperty, parameter, queryAttribute.Name); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute) - { - var headersProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers)); - paramterExpression = BindParamenter(headersProperty, parameter, headerAttribute.Name); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute) - { - if (consumeBodyDirectly) - { - throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); - } + for (var i = 0; i < parameters.Length; i++) + { + args[i] = CreateArgument(parameters[i], factoryContext); + } - if (consumeBodyAsForm) - { - ThrowCannotReadBodyDirectlyAndAsForm(); - } + return args; + } - consumeBodyDirectly = true; - allowEmptyBody = bodyAttribute.AllowEmpty; - bodyType = parameter.ParameterType; - paramterExpression = Expression.Convert(DeserializedBodyArg, bodyType); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } formAttribute) - { - if (consumeBodyDirectly) - { - ThrowCannotReadBodyDirectlyAndAsForm(); - } + private static Expression CreateArgument(ParameterInfo parameter, FactoryContext factoryContext) + { + if (parameter.Name is null) + { + throw new InvalidOperationException("A parameter does not have a name! Was it genererated? All parameters must be named."); + } - consumeBodyAsForm = true; + var parameterCustomAttributes = parameter.GetCustomAttributes(); - var formProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); - paramterExpression = BindParamenter(formProperty, parameter, parameter.Name); - } - else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) + if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute) + { + return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) + { + return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute) + { + return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute) + { + if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson) { - paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); } - else if (parameter.ParameterType == typeof(IFormCollection)) + + if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm) { - if (consumeBodyDirectly) - { - ThrowCannotReadBodyDirectlyAndAsForm(); - } + ThrowCannotReadBodyDirectlyAndAsForm(); + } - consumeBodyAsForm = true; + factoryContext.RequestBodyMode = RequestBodyMode.AsJson; + factoryContext.JsonRequestBodyType = parameter.ParameterType; + factoryContext.AllowEmptyRequestBody = bodyAttribute.AllowEmpty; - paramterExpression = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); - } - else if (parameter.ParameterType == typeof(HttpContext)) + return Expression.Convert(BodyValueExpr, parameter.ParameterType); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } formAttribute) + { + if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson) { - paramterExpression = HttpContextParameter; + ThrowCannotReadBodyDirectlyAndAsForm(); } - else if (parameter.ParameterType == typeof(CancellationToken)) + + factoryContext.RequestBodyMode = RequestBodyMode.AsForm; + + return BindParameterFromProperty(parameter, FormExpr, formAttribute.Name ?? parameter.Name, factoryContext); + } + else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) + { + return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + } + else if (parameter.ParameterType == typeof(IFormCollection)) + { + if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson) { - paramterExpression = RequestAbortedExpr; + ThrowCannotReadBodyDirectlyAndAsForm(); } - args.Add(paramterExpression); + factoryContext.RequestBodyMode = RequestBodyMode.AsForm; + + return Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); } + else if (parameter.ParameterType == typeof(HttpContext)) + { + return HttpContextExpr; + } + else if (parameter.ParameterType == typeof(CancellationToken)) + { + return RequestAbortedExpr; + } + else if (parameter.ParameterType == typeof(string) || HasTryParseMethod(parameter)) + { + return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext); + } + else + { + return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + } + } - Expression? body = null; + private static Expression CreateMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) => + target is null ? + Expression.Call(methodInfo, arguments) : + Expression.Call(target, methodInfo, arguments); - MethodCallExpression methodCall; + private static Expression CreateResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) + { + var callMethod = CreateMethodCall(methodInfo, target, arguments); + return AddResponseWritingToMethodCall(callMethod, methodInfo.ReturnType); + } - if (targetExpression is null) + // If we're calling TryParse and wasTryParseFailure indicates it failed, set a 400 StatusCode instead of calling the method. + private static Expression CreateTryParseCheckingResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) + { + // { + // bool wasTryParseFailure = false; + // string tempSourceString; + // + // // Assume "[FromRoute] int id" is the first parameter. + // + // tempSourceString = httpContext.RequestValue["id"]; + // int param1 = tempSourceString == null ? default : + // { + // int parsedValue = default; + // + // if (!int.TryParse(tempSourceString, out parsedValue)) + // { + // wasTryParseFailure = true; + // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) + // } + // + // return parsedValue; + // }; + // + // tempSourceString = httpContext.RequestValue["param2"]; + // int param2 = tempSourceString == null ? default : + // // ... + // + // return wasTryParseFailure ? + // { + // httpContext.Response.StatusCode = 400; + // return Task.CompletedTask; + // } : + // { + // // Logic generated by AddResponseWritingToMethodCall() that calls action(param1, parm2, ...) + // }; + // } + + var parameters = methodInfo.GetParameters(); + var storedArguments = new ParameterExpression[parameters.Length]; + var localVariables = new ParameterExpression[parameters.Length + 2]; + + for (var i = 0; i < parameters.Length; i++) { - methodCall = Expression.Call(methodInfo, args); + storedArguments[i] = localVariables[i] = Expression.Parameter(parameters[i].ParameterType); } - else + + localVariables[parameters.Length] = WasTryParseFailureExpr; + localVariables[parameters.Length + 1] = TempSourceStringExpr; + + var assignAndCall = new Expression[parameters.Length + 1]; + for (var i = 0; i < parameters.Length; i++) { - methodCall = Expression.Call(targetExpression, methodInfo, args); + assignAndCall[i] = Expression.Assign(localVariables[i], arguments[i]); } + var set400StatusAndReturnCompletedTask = Expression.Block( + Expression.Assign(StatusCodeExpr, Expression.Constant(400)), + CompletedTaskExpr); + + var methodCall = CreateMethodCall(methodInfo, target, storedArguments); + + var checkWasTryParseFailure = Expression.Condition(WasTryParseFailureExpr, + set400StatusAndReturnCompletedTask, + AddResponseWritingToMethodCall(methodCall, methodInfo.ReturnType)); + + assignAndCall[parameters.Length] = checkWasTryParseFailure; + + return Expression.Block(localVariables, assignAndCall); + } + + private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType) + { // Exact request delegate match - if (methodInfo.ReturnType == typeof(void)) + if (returnType == typeof(void)) { - var bodyExpressions = new List - { - methodCall, - Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo) - }; - - body = Expression.Block(bodyExpressions); + return Expression.Block(methodCall, CompletedTaskExpr); } - else if (AwaitableInfo.IsTypeAwaitable(methodInfo.ReturnType, out var info)) + else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) { - if (methodInfo.ReturnType == typeof(Task)) + if (returnType == typeof(Task)) { - body = methodCall; + return methodCall; } - else if (methodInfo.ReturnType == typeof(ValueTask)) + else if (returnType == typeof(ValueTask)) { - body = Expression.Call( - ExecuteValueTaskMethodInfo, - methodCall); + return Expression.Call( + ExecuteValueTaskMethod, + methodCall); } - else if (methodInfo.ReturnType.IsGenericType && - methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(Task<>)) { - var typeArg = methodInfo.ReturnType.GetGenericArguments()[0]; + var typeArg = returnType.GetGenericArguments()[0]; if (typeof(IResult).IsAssignableFrom(typeArg)) { - body = Expression.Call( - ExecuteTaskResultOfTMethodInfo.MakeGenericMethod(typeArg), - methodCall, - HttpContextParameter); + return Expression.Call( + ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } + // ExecuteTask(action(..), httpContext); + else if (typeArg == typeof(string)) + { + return Expression.Call( + ExecuteTaskOfStringMethod, + methodCall, + HttpContextExpr); } else { - // ExecuteTask(action(..), httpContext); - if (typeArg == typeof(string)) - { - body = Expression.Call( - ExecuteTaskOfStringMethodInfo, - methodCall, - HttpContextParameter); - } - else - { - body = Expression.Call( - ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg), - methodCall, - HttpContextParameter); - } + return Expression.Call( + ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } } - else if (methodInfo.ReturnType.IsGenericType && - methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) { - var typeArg = methodInfo.ReturnType.GetGenericArguments()[0]; + var typeArg = returnType.GetGenericArguments()[0]; if (typeof(IResult).IsAssignableFrom(typeArg)) { - body = Expression.Call( - ExecuteValueResultTaskOfTMethodInfo.MakeGenericMethod(typeArg), - methodCall, - HttpContextParameter); + return Expression.Call( + ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } + // ExecuteTask(action(..), httpContext); + else if (typeArg == typeof(string)) + { + return Expression.Call( + ExecuteValueTaskOfStringMethod, + methodCall, + HttpContextExpr); } else { - // ExecuteTask(action(..), httpContext); - if (typeArg == typeof(string)) - { - body = Expression.Call( - ExecuteValueTaskOfStringMethodInfo, - methodCall, - HttpContextParameter); - } - else - { - body = Expression.Call( - ExecuteValueTaskOfTMethodInfo.MakeGenericMethod(typeArg), - methodCall, - HttpContextParameter); - } + return Expression.Call( + ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } } else { // TODO: Handle custom awaitables - throw new NotSupportedException($"Unsupported return type: {methodInfo.ReturnType}"); + throw new NotSupportedException($"Unsupported return type: {returnType}"); } } - else if (typeof(IResult).IsAssignableFrom(methodInfo.ReturnType)) + else if (typeof(IResult).IsAssignableFrom(returnType)) { - body = Expression.Call(methodCall, ResultWriteResponseAsync, HttpContextParameter); + return Expression.Call(methodCall, ResultWriteResponseAsyncMethod, HttpContextExpr); } - else if (methodInfo.ReturnType == typeof(string)) + else if (returnType == typeof(string)) { - body = Expression.Call(StringResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + return Expression.Call(StringResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); } - else if (methodInfo.ReturnType.IsValueType) + else if (returnType.IsValueType) { var box = Expression.TypeAs(methodCall, typeof(object)); - body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); } else { - body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); } + } - Func? requestDelegate = null; - - if (consumeBodyDirectly) + private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) + { + if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson) { // We need to generate the code for reading from the body before calling into the delegate - var lambda = Expression.Lambda>(body, TargetArg, HttpContextParameter, DeserializedBodyArg); - var invoker = lambda.Compile(); + var invoker = Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile(); + + var bodyType = factoryContext.JsonRequestBodyType!; object? defaultBodyValue = null; - if (allowEmptyBody && bodyType!.IsValueType) + if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType) { defaultBodyValue = Activator.CreateInstance(bodyType); } - requestDelegate = async (target, httpContext) => + return async (target, httpContext) => { object? bodyValue; - if (allowEmptyBody && httpContext.Request.ContentLength == 0) + if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0) { bodyValue = defaultBodyValue; } @@ -378,17 +472,16 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func>(body, TargetArg, HttpContextParameter); - var invoker = lambda.Compile(); + var invoker = Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile(); - requestDelegate = async (target, httpContext) => + return async (target, httpContext) => { // Generating async code would just be insane so if the method needs the form populate it here // so the within the method it's cached @@ -412,13 +505,13 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func>(body, TargetArg, HttpContextParameter); - var invoker = lambda.Compile(); - - requestDelegate = invoker; + return Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile(); } - - return requestDelegate; } - private static ILogger GetLogger(HttpContext httpContext) + private static MethodInfo GetEnumTryParseMethod() { - var loggerFactory = httpContext.RequestServices.GetRequiredService(); - return loggerFactory.CreateLogger("Microsoft.AspNetCore.Routing.MapAction"); + var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static); + + foreach (var method in staticEnumMethods) + { + if (!method.IsGenericMethod || method.Name != "TryParse" || method.ReturnType != typeof(bool)) + { + continue; + } + + var tryParseParameters = method.GetParameters(); + + if (tryParseParameters.Length == 2 && + tryParseParameters[0].ParameterType == typeof(string) && + tryParseParameters[1].IsOut) + { + return method; + } + } + + throw new Exception("static bool System.Enum.TryParse(string? value, out TEnum result) does not exist!!?!?"); } - private static Expression BindParamenter(Expression sourceExpression, ParameterInfo parameter, string? name) + // TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible? + private static MethodInfo? FindTryParseMethod(Type type) { - var key = name ?? parameter.Name; - var type = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType; - var valueArg = Expression.Convert( - Expression.MakeIndex(sourceExpression, - sourceExpression.Type.GetProperty("Item"), - new[] { Expression.Constant(key) }), - typeof(string)); + static MethodInfo? Finder(Type type) + { + if (type.IsEnum) + { + return EnumTryParseMethod.MakeGenericMethod(type); + } - MethodInfo parseMethod = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Static) - let parameters = m.GetParameters() - where m.Name == "Parse" && parameters.Length == 1 && parameters[0].ParameterType == typeof(string) - select m).FirstOrDefault()!; + var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static); - Expression? expr = null; + foreach (var method in staticMethods) + { + if (method.Name != "TryParse" || method.ReturnType != typeof(bool)) + { + continue; + } - if (parseMethod != null) - { - expr = Expression.Call(parseMethod, valueArg); - } - else if (parameter.ParameterType != valueArg.Type) - { - // Convert.ChangeType() - expr = Expression.Call(ChangeTypeMethodInfo, valueArg, Expression.Constant(type)); - } - else - { - expr = valueArg; + var tryParseParameters = method.GetParameters(); + + if (tryParseParameters.Length == 2 && + tryParseParameters[0].ParameterType == typeof(string) && + tryParseParameters[1].IsOut && + tryParseParameters[1].ParameterType == type.MakeByRefType()) + { + return method; + } + } + + return null; } - if (expr.Type != parameter.ParameterType) + return TryParseMethodCache.GetOrAdd(type, Finder); + } + + private static bool HasTryParseMethod(ParameterInfo parameter) + { + var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType; + return FindTryParseMethod(nonNullableParameterType) is not null; + } + + private static Expression GetValueFromProperty(Expression sourceExpression, string key) + { + var itemProperty = sourceExpression.Type.GetProperty("Item"); + var indexArguments = new[] { Expression.Constant(key) }; + var indexExpression = Expression.MakeIndex(sourceExpression, itemProperty, indexArguments); + return Expression.Convert(indexExpression, typeof(string)); + } + + private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext) + { + if (parameter.ParameterType == typeof(string)) { - expr = Expression.Convert(expr, parameter.ParameterType); + return valueExpression; } - Expression defaultExpression; - if (parameter.HasDefaultValue) + var underlyingNullableType = Nullable.GetUnderlyingType(parameter.ParameterType); + var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType; + var tryParseMethod = FindTryParseMethod(nonNullableParameterType); + + if (tryParseMethod is null) { - defaultExpression = Expression.Constant(parameter.DefaultValue); + throw new InvalidOperationException($"No public static bool {parameter.ParameterType.Name}.TryParse(string, out {parameter.ParameterType.Name}) method found for {parameter.Name}."); } - else + + // bool wasTryParseFailure = false; + // string tempSourceString; + // + // // Assume "[FromRoute] int id" is the first parameter. + // tempSourceString = httpContext.RequestValue["id"]; + // + // int param1 = tempSourceString == null ? default : + // { + // int parsedValue = default; + // + // if (!int.TryParse(tempSourceString, out parsedValue)) + // { + // wasTryParseFailure = true; + // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) + // } + // + // return parsedValue; + // }; + + factoryContext.CheckForTryParseFailure = true; + + var parsedValue = Expression.Variable(nonNullableParameterType); + + var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name); + var parameterNameConstant = Expression.Constant(parameter.Name); + + var failBlock = Expression.Block( + Expression.Assign(WasTryParseFailureExpr, Expression.Constant(true)), + Expression.Call(LogParameterBindingFailureMethod, + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr)); + + var tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue); + var ifFailExpression = Expression.IfThen(Expression.Not(tryParseCall), failBlock); + + Expression parsedValueExpression = Expression.Block(new[] { parsedValue }, + ifFailExpression, + parsedValue); + + // Convert back to nullable if necessary. + if (underlyingNullableType is not null) { - defaultExpression = Expression.Default(parameter.ParameterType); + parsedValueExpression = Expression.Convert(parsedValueExpression, parameter.ParameterType); } - // property[key] == null ? default : (ParameterType){Type}.Parse(property[key]); - expr = Expression.Condition( - Expression.Equal(valueArg, Expression.Constant(null)), + Expression defaultExpression = parameter.HasDefaultValue ? + Expression.Constant(parameter.DefaultValue) : + Expression.Default(parameter.ParameterType); + + // tempSourceString = httpContext.RequestValue["id"]; + var storeValueToTemp = Expression.Assign(TempSourceStringExpr, valueExpression); + + // int param1 = tempSourcString == null ? default : ... + var ternary = Expression.Condition( + Expression.Equal(TempSourceStringExpr, Expression.Constant(null)), defaultExpression, - expr); + parsedValueExpression); + + return Expression.Block(storeValueToTemp, ternary); + } - return expr; + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext) => + BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext); + + private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) + { + var routeValue = GetValueFromProperty(RouteValuesExpr, key); + var queryValue = GetValueFromProperty(QueryExpr, key); + return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext); } private static MethodInfo GetMethodInfo(Expression expr) @@ -611,6 +799,22 @@ private static void ThrowCannotReadBodyDirectlyAndAsForm() throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method."); } + private enum RequestBodyMode + { + None, + AsJson, + AsForm, + } + + private class FactoryContext + { + public RequestBodyMode RequestBodyMode { get; set; } + public Type? JsonRequestBodyType { get; set; } + public bool AllowEmptyRequestBody { get; set; } + + public bool CheckForTryParseFailure { get; set; } + } + private static class Log { private static readonly Action _requestBodyIOException = LoggerMessage.Define( @@ -623,14 +827,30 @@ private static class Log new EventId(2, "RequestBodyInvalidDataException"), "Reading the request body failed with an InvalidDataException."); - public static void RequestBodyIOException(ILogger logger, IOException exception) + private static readonly Action _parameterBindingFailed = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, "ParamaterBindingFailed"), + @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""."); + + public static void RequestBodyIOException(HttpContext httpContext, IOException exception) + { + _requestBodyIOException(GetLogger(httpContext), exception); + } + + public static void RequestBodyInvalidDataException(HttpContext httpContext, InvalidDataException exception) + { + _requestBodyInvalidDataException(GetLogger(httpContext), exception); + } + + public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue) { - _requestBodyIOException(logger, exception); + _parameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue, null); } - public static void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception) + private static ILogger GetLogger(HttpContext httpContext) { - _requestBodyInvalidDataException(logger, exception); + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + return loggerFactory.CreateLogger(typeof(RequestDelegateFactory)); } } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 36d4004cfbe3..80e4532746fe 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -7,7 +7,12 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq.Expressions; +using System.Net; +using System.Net.Sockets; +using System.Numerics; using System.Reflection; +using System.Reflection.Metadata; using System.Text; using System.Text.Json; using System.Threading; @@ -15,15 +20,15 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Routing.Internal { - public class RequestDelegateFactoryTests + public class RequestDelegateFactoryTests : LoggedTest { public static IEnumerable NoResult { @@ -124,7 +129,7 @@ public TestNonStaticActionClass(object invokedValue) _invokedValue = invokedValue; } - private void NonStaticTestAction(HttpContext httpContext) + public void NonStaticTestAction(HttpContext httpContext) { httpContext.Items.Add("invoked", _invokedValue); } @@ -132,10 +137,11 @@ private void NonStaticTestAction(HttpContext httpContext) [Fact] public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection() + { var methodInfo = typeof(TestNonStaticActionClass).GetMethod( - "NonStaticTestAction", - BindingFlags.NonPublic | BindingFlags.Instance, + nameof(TestNonStaticActionClass.NonStaticTestAction), + BindingFlags.Public | BindingFlags.Instance, new[] { typeof(HttpContext) }); var invoked = false; @@ -185,103 +191,46 @@ public void BuildRequestDelegateThrowsArgumentNullExceptions() Assert.Equal("targetFactory", exNullTargetFactory.ParamName); } - public static IEnumerable FromRouteResult - { - get - { - void TestAction(HttpContext httpContext, [FromRoute] int value) - { - StoreInput(httpContext, value); - }; - - Task TaskTestAction(HttpContext httpContext, [FromRoute] int value) - { - StoreInput(httpContext, value); - return Task.CompletedTask; - } - - ValueTask ValueTaskTestAction(HttpContext httpContext, [FromRoute] int value) - { - StoreInput(httpContext, value); - return ValueTask.CompletedTask; - } - - return new List - { - new object[] { (Action)TestAction }, - new object[] { (Func)TaskTestAction }, - new object[] { (Func)ValueTaskTestAction }, - }; - } - } - private static void StoreInput(HttpContext httpContext, object value) - { - httpContext.Items.Add("input", value); - } - - [Theory] - [MemberData(nameof(FromRouteResult))] - public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName(Delegate @delegate) + [Fact] + public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName() { const string paramName = "value"; const int originalRouteParam = 42; + void TestAction(HttpContext httpContext, [FromRoute] int value) + { + httpContext.Items.Add("input", value); + } + var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction); await requestDelegate(httpContext); - Assert.Equal(originalRouteParam, httpContext.Items["input"] as int?); - } - - public static IEnumerable FromRouteOptionalResult - { - get - { - return new List - { - new object[] { (Action)TestAction }, - new object[] { (Func)TaskTestAction }, - new object[] { (Func)ValueTaskTestAction } - }; - } + Assert.Equal(originalRouteParam, httpContext.Items["input"]); } private static void TestAction(HttpContext httpContext, [FromRoute] int value = 42) { - StoreInput(httpContext, value); - } - - private static Task TaskTestAction(HttpContext httpContext, [FromRoute] int value = 42) - { - StoreInput(httpContext, value); - return Task.CompletedTask; - } - - private static ValueTask ValueTaskTestAction(HttpContext httpContext, [FromRoute] int value = 42) - { - StoreInput(httpContext, value); - return ValueTask.CompletedTask; + httpContext.Items.Add("input", value); } - [Theory] - [MemberData(nameof(FromRouteOptionalResult))] - public async Task RequestDelegatePopulatesFromRouteOptionalParameter(Delegate @delegate) + [Fact] + public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction); await requestDelegate(httpContext); - Assert.Equal(42, httpContext.Items["input"] as int?); + Assert.Equal(42, httpContext.Items["input"]); } - [Theory] - [MemberData(nameof(FromRouteOptionalResult))] - public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName(Delegate @delegate) + [Fact] + public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName() { const string paramName = "value"; const int originalRouteParam = 47; @@ -290,11 +239,11 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create(@delegate); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction); await requestDelegate(httpContext); - Assert.Equal(47, httpContext.Items["input"] as int?); + Assert.Equal(47, httpContext.Items["input"]); } [Fact] @@ -343,6 +292,206 @@ void TestAction([FromRoute] int foo) Assert.Equal(0, deserializedRouteParam); } + public static object?[][] TryParsableParameters + { + get + { + static void Store(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)Store, "plain string", "plain string" }, + new object[] { (Action)Store, "-42", -42 }, + new object[] { (Action)Store, "42", 42U }, + new object[] { (Action)Store, "true", true }, + new object[] { (Action)Store, "-42", (short)-42 }, + new object[] { (Action)Store, "42", (ushort)42 }, + new object[] { (Action)Store, "-42", -42L }, + new object[] { (Action)Store, "42", 42UL }, + new object[] { (Action)Store, "-42", new IntPtr(-42) }, + new object[] { (Action)Store, "A", 'A' }, + new object[] { (Action)Store, "0.5", 0.5 }, + new object[] { (Action)Store, "0.5", 0.5f }, + new object[] { (Action)Store, "0.5", (Half)0.5f }, + new object[] { (Action)Store, "0.5", 0.5m }, + new object[] { (Action)Store, now.ToString("o"), now }, + new object[] { (Action)Store, "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch }, + new object[] { (Action)Store, "00:00:42", TimeSpan.FromSeconds(42) }, + new object[] { (Action)Store, "00000000-0000-0000-0000-000000000000", Guid.Empty }, + new object[] { (Action)Store, "6.0.0.42", new Version("6.0.0.42") }, + new object[] { (Action)Store, "-42", new BigInteger(-42) }, + new object[] { (Action)Store, "127.0.0.1", IPAddress.Loopback }, + new object[] { (Action)Store, "127.0.0.1:80", new IPEndPoint(IPAddress.Loopback, 80) }, + new object[] { (Action)Store, "Unix", AddressFamily.Unix }, + new object[] { (Action)Store, "Nop", ILOpCode.Nop }, + new object[] { (Action)Store, "PublicKey,Retargetable", AssemblyFlags.PublicKey | AssemblyFlags.Retargetable }, + new object[] { (Action)Store, "42", 42 }, + new object[] { (Action)Store, "ValueB", MyEnum.ValueB }, + new object[] { (Action)Store, "https://example.org", new MyTryParsableRecord(new Uri("https://example.org")) }, + new object?[] { (Action)Store, null, 0 }, + new object?[] { (Action)Store, null, null }, + }; + } + } + + private enum MyEnum { ValueA, ValueB, } + + private record MyTryParsableRecord(Uri Uri) + { + public static bool TryParse(string? value, out MyTryParsableRecord? result) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) + { + result = null; + return false; + } + + result = new MyTryParsableRecord(uri); + return true; + } + } + + [Theory] + [MemberData(nameof(TryParsableParameters))] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue) + { + var invalidDataException = new InvalidDataException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = routeValue; + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = RequestDelegateFactory.Create(action); + + await requestDelegate(httpContext); + + Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); + } + + [Theory] + [MemberData(nameof(TryParsableParameters))] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQueryString(Delegate action, string? routeValue, object? expectedParameterValue) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["tryParsable"] = routeValue + }); + + var requestDelegate = RequestDelegateFactory.Create(action); + + await requestDelegate(httpContext); + + Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); + } + + [Fact] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValueBeforeQueryString() + { + var httpContext = new DefaultHttpContext(); + + httpContext.Request.RouteValues["tryParsable"] = "42"; + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["tryParsable"] = "invalid!" + }); + + var requestDelegate = RequestDelegateFactory.Create((Action)((httpContext, tryParsable) => + { + httpContext.Items["tryParsable"] = tryParsable; + })); + + await requestDelegate(httpContext); + + Assert.Equal(42, httpContext.Items["tryParsable"]); + } + + public static object[][] DelegatesWithInvalidAttributes + { + get + { + void InvalidFromRoute([FromRoute] object notTryParsable) { } + void InvalidFromQuery([FromQuery] object notTryParsable) { } + void InvalidFromHeader([FromHeader] object notTryParsable) { } + void InvalidFromForm([FromForm] object notTryParsable) { } + + return new[] + { + new object[] { (Action)InvalidFromRoute }, + new object[] { (Action)InvalidFromQuery }, + new object[] { (Action)InvalidFromHeader }, + new object[] { (Action)InvalidFromForm }, + }; + } + } + + [Theory] + [MemberData(nameof(DelegatesWithInvalidAttributes))] + public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) + { + var ex = Assert.Throws(() => RequestDelegateFactory.Create(action)); + Assert.Equal("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(() => RequestDelegateFactory.Create((Action)lambda.Compile())); + Assert.Equal("A parameter does not have a name! Was it genererated? All parameters must be named.", ex.Message); + } + + [Fact] + public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response() + { + var invoked = false; + + void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) + { + invoked = true; + } + + var invalidDataException = new InvalidDataException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = "invalid!"; + httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + + var logs = TestSink.Writes.ToArray(); + + Assert.Equal(2, logs.Length); + + Assert.Equal(new EventId(3, "ParamaterBindingFailed"), logs[0].EventId); + Assert.Equal(LogLevel.Debug, logs[0].LogLevel); + Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable"" from ""invalid!"".", logs[0].Message); + + Assert.Equal(new EventId(3, "ParamaterBindingFailed"), logs[0].EventId); + Assert.Equal(LogLevel.Debug, logs[0].LogLevel); + Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable2"" from ""invalid again!"".", logs[1].Message); + } + [Fact] public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName() { @@ -485,13 +634,10 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) } [Fact] - public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndAborts() + public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndDoesNotAbort() { var invoked = false; - var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); - var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); - void TestAction([FromBody] Todo todo) { invoked = true; @@ -499,7 +645,7 @@ void TestAction([FromBody] Todo todo) var ioException = new IOException(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(testLoggerFactory); + serviceCollection.AddSingleton(LoggerFactory); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; @@ -512,9 +658,9 @@ void TestAction([FromBody] Todo todo) await requestDelegate(httpContext); Assert.False(invoked); - Assert.True(httpContext.RequestAborted.IsCancellationRequested); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var logMessage = Assert.Single(sink.Writes); + var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); Assert.Same(ioException, logMessage.Exception); @@ -525,9 +671,6 @@ public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndSets { var invoked = false; - var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); - var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); - void TestAction([FromBody] Todo todo) { invoked = true; @@ -535,7 +678,7 @@ void TestAction([FromBody] Todo todo) var invalidDataException = new InvalidDataException(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(testLoggerFactory); + serviceCollection.AddSingleton(LoggerFactory); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; @@ -551,7 +694,7 @@ void TestAction([FromBody] Todo todo) Assert.False(httpContext.RequestAborted.IsCancellationRequested); Assert.Equal(400, httpContext.Response.StatusCode); - var logMessage = Assert.Single(sink.Writes); + var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); Assert.Same(invalidDataException, logMessage.Exception); @@ -590,9 +733,6 @@ public async Task RequestDelegateLogsFromFormIOExceptionsAsDebugAndAborts() { var invoked = false; - var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); - var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); - void TestAction([FromForm] int value) { invoked = true; @@ -600,7 +740,7 @@ void TestAction([FromForm] int value) var ioException = new IOException(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(testLoggerFactory); + serviceCollection.AddSingleton(LoggerFactory); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -615,7 +755,7 @@ void TestAction([FromForm] int value) Assert.False(invoked); Assert.True(httpContext.RequestAborted.IsCancellationRequested); - var logMessage = Assert.Single(sink.Writes); + var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); Assert.Same(ioException, logMessage.Exception); @@ -626,9 +766,6 @@ public async Task RequestDelegateLogsFromFormInvalidDataExceptionsAsDebugAndSets { var invoked = false; - var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); - var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); - void TestAction([FromForm] int value) { invoked = true; @@ -636,7 +773,7 @@ void TestAction([FromForm] int value) var invalidDataException = new InvalidDataException(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(testLoggerFactory); + serviceCollection.AddSingleton(LoggerFactory); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -652,7 +789,7 @@ void TestAction([FromForm] int value) Assert.False(httpContext.RequestAborted.IsCancellationRequested); Assert.Equal(400, httpContext.Response.StatusCode); - var logMessage = Assert.Single(sink.Writes); + var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); Assert.Same(invalidDataException, logMessage.Exception); @@ -676,16 +813,33 @@ void TestAction([FromBody] int value1, [FromBody] int value2) { } Assert.Throws(() => RequestDelegateFactory.Create((Action)TestAction)); } - [Fact] - public async Task RequestDelegatePopulatesFromServiceParameterBasedOnParameterType() + public static object[][] FromServiceParameter { - var myOriginalService = new MyService(); - MyService? injectedService = null; - - void TestAction([FromService] MyService myService) + get { - injectedService = myService; + void TestExplicitFromService(HttpContext httpContext, [FromService] MyService myService) + { + httpContext.Items.Add("service", myService); + } + + void TestImpliedFromService(HttpContext httpContext, MyService myService) + { + httpContext.Items.Add("service", myService); + } + + return new[] + { + new[] { (Action)TestExplicitFromService }, + new[] { (Action)TestImpliedFromService }, + }; } + } + + [Theory] + [MemberData(nameof(FromServiceParameter))] + public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action) + { + var myOriginalService = new MyService(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(myOriginalService); @@ -693,11 +847,23 @@ void TestAction([FromService] MyService myService) var httpContext = new DefaultHttpContext(); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction); + var requestDelegate = RequestDelegateFactory.Create((Action)action); await requestDelegate(httpContext); - Assert.Same(myOriginalService, injectedService); + Assert.Same(myOriginalService, httpContext.Items["service"]); + } + + [Theory] + [MemberData(nameof(FromServiceParameter))] + public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Delegate action) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = (new ServiceCollection()).BuildServiceProvider(); + + var requestDelegate = RequestDelegateFactory.Create((Action)action); + + await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } [Fact] diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 58b8f6f80b9d..6fad000bf864 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -1,4 +1,4 @@ -{ +{ "solution": { "path": "..\\..\\AspNetCore.sln", "projects": [ @@ -11,13 +11,14 @@ "src\\Http\\Authentication.Core\\test\\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", "src\\Http\\Headers\\test\\Microsoft.Net.Http.Headers.Tests.csproj", + "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http.Features\\test\\Microsoft.AspNetCore.Http.Features.Tests.csproj", - "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj", + "src\\Http\\Http\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Microbenchmarks.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", @@ -25,19 +26,17 @@ "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj", - "src\\Http\\Http\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Microbenchmarks.csproj", - "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj", - "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj", "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj", "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj", - "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", + "src\\Http\\samples\\MapActionSample\\MapActionSample.csproj", "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", @@ -50,8 +49,7 @@ "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", - "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj", - "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj" + "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } } \ No newline at end of file diff --git a/src/Http/Routing/samples/MapActionSample/Program.cs b/src/Http/Routing/samples/MapActionSample/Program.cs deleted file mode 100644 index 0e33ff8e1f6f..000000000000 --- a/src/Http/Routing/samples/MapActionSample/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace HttpApiSampleApp -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/src/Http/Routing/samples/MapActionSample/Startup.cs b/src/Http/Routing/samples/MapActionSample/Startup.cs deleted file mode 100644 index e3c44398834b..000000000000 --- a/src/Http/Routing/samples/MapActionSample/Startup.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace HttpApiSampleApp -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - JsonResult EchoTodo([FromBody] Todo todo) => new(todo); - - endpoints.MapPost("/EchoTodo", (Func)EchoTodo); - - endpoints.MapPost("/EchoTodoProto", async httpContext => - { - var todo = await httpContext.Request.ReadFromJsonAsync(); - await httpContext.Response.WriteAsJsonAsync(todo); - }); - - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Hello World!"); - }); - }); - } - } -} diff --git a/src/Http/Routing/samples/MapActionSample/Todo.cs b/src/Http/Routing/samples/MapActionSample/Todo.cs deleted file mode 100644 index 2bcc698c8e5a..000000000000 --- a/src/Http/Routing/samples/MapActionSample/Todo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HttpApiSampleApp -{ - public class Todo - { - public int Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - } -} diff --git a/src/Http/Routing/samples/MapActionSample/MapActionSample.csproj b/src/Http/samples/MapActionSample/MapActionSample.csproj similarity index 100% rename from src/Http/Routing/samples/MapActionSample/MapActionSample.csproj rename to src/Http/samples/MapActionSample/MapActionSample.csproj diff --git a/src/Http/samples/MapActionSample/Program.cs b/src/Http/samples/MapActionSample/Program.cs new file mode 100644 index 000000000000..1df9c761fcb2 --- /dev/null +++ b/src/Http/samples/MapActionSample/Program.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using var host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Configure(app => + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + Todo EchoTodo([FromBody] Todo todo) => todo; + endpoints.MapPost("/EchoTodo", (Func)EchoTodo); + + string Plaintext() => "Hello, World!"; + endpoints.MapGet("/plaintext", (Func)Plaintext); + + object Json() => new { message = "Hello, World!" }; + endpoints.MapGet("/json", (Func)Json); + }); + + }); + }) + .Build(); + +await host.RunAsync(); + +record Todo(int Id, string Name, bool IsComplete); diff --git a/src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json b/src/Http/samples/MapActionSample/Properties/launchSettings.json similarity index 100% rename from src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json rename to src/Http/samples/MapActionSample/Properties/launchSettings.json diff --git a/src/Http/Routing/samples/MapActionSample/appsettings.Development.json b/src/Http/samples/MapActionSample/appsettings.Development.json similarity index 100% rename from src/Http/Routing/samples/MapActionSample/appsettings.Development.json rename to src/Http/samples/MapActionSample/appsettings.Development.json diff --git a/src/Http/Routing/samples/MapActionSample/appsettings.json b/src/Http/samples/MapActionSample/appsettings.json similarity index 100% rename from src/Http/Routing/samples/MapActionSample/appsettings.json rename to src/Http/samples/MapActionSample/appsettings.json