Skip to content

Commit 54f36e4

Browse files
authored
Merge pull request #1115 from aws/normj/fix-minimalapi-post
Fixed issue with ASP.NET Core Minimal API MapPost with complex types
2 parents 599fc8d + 9778c83 commit 54f36e4

File tree

7 files changed

+348
-7
lines changed

7 files changed

+348
-7
lines changed

Libraries/Libraries.sln

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
44
VisualStudioVersion = 17.0.31717.71
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}"
7-
ProjectSection(SolutionItems) = preProject
8-
src\Amazon.Lambda.Annotations.nuspec = src\Amazon.Lambda.Annotations.nuspec
9-
EndProjectSection
7+
ProjectSection(SolutionItems) = preProject
8+
src\Amazon.Lambda.Annotations.nuspec = src\Amazon.Lambda.Annotations.nuspec
9+
EndProjectSection
1010
EndProject
1111
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A101C2F7-C63F-4FDE-94BB-DFA637EBEA44}"
1212
ProjectSection(SolutionItems) = preProject
@@ -106,13 +106,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HandlerTestNoSerializer", "
106106
EndProject
107107
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestServerlessApp", "test\TestServerlessApp\TestServerlessApp.csproj", "{3D322CAB-0DDD-4C84-B3ED-0862F244AF5C}"
108108
EndProject
109-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.Annotations.SourceGenerators.Tests", "test\Amazon.Lambda.Annotations.SourceGenerators.Tests\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj", "{D76F2C74-3D7F-4DB3-BA1A-F2EA14749253}"
109+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Annotations.SourceGenerators.Tests", "test\Amazon.Lambda.Annotations.SourceGenerators.Tests\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj", "{D76F2C74-3D7F-4DB3-BA1A-F2EA14749253}"
110110
EndProject
111-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.Annotations", "src\Amazon.Lambda.Annotations\Amazon.Lambda.Annotations.csproj", "{ADA9AF37-A8C1-47E6-BBBD-5C7E49C26C0E}"
111+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Annotations", "src\Amazon.Lambda.Annotations\Amazon.Lambda.Annotations.csproj", "{ADA9AF37-A8C1-47E6-BBBD-5C7E49C26C0E}"
112112
EndProject
113-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.Annotations.SourceGenerator", "src\Amazon.Lambda.Annotations.SourceGenerator\Amazon.Lambda.Annotations.SourceGenerator.csproj", "{3C617909-D61F-4AA8-B11C-4C9ECC865D75}"
113+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Annotations.SourceGenerator", "src\Amazon.Lambda.Annotations.SourceGenerator\Amazon.Lambda.Annotations.SourceGenerator.csproj", "{3C617909-D61F-4AA8-B11C-4C9ECC865D75}"
114114
EndProject
115-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerlessApp.IntegrationTests", "test\TestServerlessApp.IntegrationTests\TestServerlessApp.IntegrationTests.csproj", "{2D956162-04BE-402E-9487-AE785AA14DE4}"
115+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestServerlessApp.IntegrationTests", "test\TestServerlessApp.IntegrationTests\TestServerlessApp.IntegrationTests.csproj", "{2D956162-04BE-402E-9487-AE785AA14DE4}"
116116
EndProject
117117
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AspNetCoreServer.Hosting", "src\Amazon.Lambda.AspNetCoreServer.Hosting\Amazon.Lambda.AspNetCoreServer.Hosting.csproj", "{02908C6F-FBDF-4949-B039-0F4632265B90}"
118118
EndProject
@@ -122,6 +122,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventsTests.NET6", "test\Ev
122122
EndProject
123123
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.KafkaEvents", "src\Amazon.Lambda.KafkaEvents\Amazon.Lambda.KafkaEvents.csproj", "{982A26C7-A5D1-4783-A7F8-F2B28AA2459E}"
124124
EndProject
125+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestMinimalAPIApp", "test\TestMinimalAPIApp\TestMinimalAPIApp.csproj", "{8AB1CBD7-2D08-492F-9C09-3E754364046C}"
126+
EndProject
125127
Global
126128
GlobalSection(SharedMSBuildProjectFiles) = preSolution
127129
test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5
@@ -329,6 +331,10 @@ Global
329331
{982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Debug|Any CPU.Build.0 = Debug|Any CPU
330332
{982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Release|Any CPU.ActiveCfg = Release|Any CPU
331333
{982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Release|Any CPU.Build.0 = Release|Any CPU
334+
{8AB1CBD7-2D08-492F-9C09-3E754364046C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
335+
{8AB1CBD7-2D08-492F-9C09-3E754364046C}.Debug|Any CPU.Build.0 = Debug|Any CPU
336+
{8AB1CBD7-2D08-492F-9C09-3E754364046C}.Release|Any CPU.ActiveCfg = Release|Any CPU
337+
{8AB1CBD7-2D08-492F-9C09-3E754364046C}.Release|Any CPU.Build.0 = Release|Any CPU
332338
EndGlobalSection
333339
GlobalSection(SolutionProperties) = preSolution
334340
HideSolutionNode = FALSE
@@ -387,6 +393,7 @@ Global
387393
{2FFBE745-B7D5-4E44-B76D-88A0C2402FEB} = {B5BD0336-7D08-492C-8489-42C987E29B39}
388394
{C1BB30D2-3237-4CFC-BA93-627471148EC2} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
389395
{982A26C7-A5D1-4783-A7F8-F2B28AA2459E} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
396+
{8AB1CBD7-2D08-492F-9C09-3E754364046C} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
390397
EndGlobalSection
391398
GlobalSection(ExtensibilityGlobals) = postSolution
392399
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}

Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public class InvokeFeatures : IFeatureCollection,
2828
ITlsConnectionFeature
2929

3030
,IHttpResponseBodyFeature
31+
32+
#if NET6_0_OR_GREATER
33+
,IHttpRequestBodyDetectionFeature
34+
#endif
3135
/*
3236
,
3337
IHttpUpgradeFeature,
@@ -46,6 +50,10 @@ public InvokeFeatures()
4650
this[typeof(IServiceProvidersFeature)] = this;
4751
this[typeof(ITlsConnectionFeature)] = this;
4852
this[typeof(IHttpResponseBodyFeature)] = this;
53+
54+
#if NET6_0_OR_GREATER
55+
this[typeof(IHttpRequestBodyDetectionFeature)] = this;
56+
#endif
4957
}
5058

5159
#region IFeatureCollection
@@ -345,5 +353,16 @@ public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancel
345353
public X509Certificate2 ClientCertificate { get; set; }
346354

347355
#endregion
356+
357+
#if NET6_0_OR_GREATER
358+
bool IHttpRequestBodyDetectionFeature.CanHaveBody
359+
{
360+
get
361+
{
362+
var requestFeature = (IHttpRequestFeature)this;
363+
return requestFeature.Body != null;
364+
}
365+
}
366+
#endif
348367
}
349368
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Reflection;
8+
using System.Runtime.InteropServices;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
using Amazon.Lambda.Serialization.SystemTextJson;
13+
using Amazon.Lambda.APIGatewayEvents;
14+
using Xunit;
15+
16+
namespace Amazon.Lambda.AspNetCoreServer.Test
17+
{
18+
public class TestMinimalAPI : IClassFixture<TestMinimalAPI.TestMinimalAPIAppFixture>
19+
{
20+
TestMinimalAPIAppFixture _fixture;
21+
22+
public TestMinimalAPI(TestMinimalAPI.TestMinimalAPIAppFixture fixture)
23+
{
24+
this._fixture = fixture;
25+
}
26+
27+
[Fact]
28+
public void TestMapPostComplexType()
29+
{
30+
var response = _fixture.ExecuteRequest<APIGatewayProxyResponse>("minimal-api-post.json");
31+
Assert.Equal((int)HttpStatusCode.OK, response.StatusCode);
32+
Assert.Contains("works:string", response.Body);
33+
}
34+
35+
public class TestMinimalAPIAppFixture : IDisposable
36+
{
37+
object lock_process = new object();
38+
public TestMinimalAPIAppFixture()
39+
{
40+
}
41+
42+
public void Dispose()
43+
{
44+
}
45+
46+
47+
public T ExecuteRequest<T>(string eventFilePath)
48+
{
49+
var requestFilePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), eventFilePath);
50+
var responseFilePath = Path.GetTempFileName();
51+
52+
var comamndArgument = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c" : $"-c";
53+
ProcessStartInfo processStartInfo = new ProcessStartInfo();
54+
processStartInfo.FileName = GetSystemShell();
55+
processStartInfo.Arguments = $"{comamndArgument} dotnet run \"{requestFilePath}\" \"{responseFilePath}\"";
56+
processStartInfo.WorkingDirectory = GetTestAppDirectory();
57+
58+
59+
lock (lock_process)
60+
{
61+
using var process = Process.Start(processStartInfo);
62+
process.WaitForExit(15000);
63+
64+
if(!File.Exists(responseFilePath))
65+
{
66+
throw new Exception("No response file found");
67+
}
68+
69+
using var responseFileStream = File.OpenRead(responseFilePath);
70+
71+
var serializer = new DefaultLambdaJsonSerializer();
72+
var response = serializer.Deserialize<T>(responseFileStream);
73+
74+
return response;
75+
}
76+
}
77+
78+
private string GetTestAppDirectory()
79+
{
80+
var path = this.GetType().GetTypeInfo().Assembly.Location;
81+
while(!string.Equals(new DirectoryInfo(path).Name, "test"))
82+
{
83+
path = Directory.GetParent(path).FullName;
84+
}
85+
86+
return Path.GetFullPath(Path.Combine(path, "TestMinimalAPIApp"));
87+
}
88+
89+
private string GetSystemShell()
90+
{
91+
if (TryGetEnvironmentVariable("COMSPEC", out var comspec))
92+
{
93+
return comspec!;
94+
}
95+
96+
if (TryGetEnvironmentVariable("SHELL", out var shell))
97+
{
98+
return shell!;
99+
}
100+
101+
// fallback to defaults
102+
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/sh";
103+
}
104+
105+
private bool TryGetEnvironmentVariable(string variable, out string? value)
106+
{
107+
value = Environment.GetEnvironmentVariable(variable);
108+
return !string.IsNullOrEmpty(value);
109+
}
110+
}
111+
}
112+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
{
2+
"resource": "/{proxy+}",
3+
"path": "/test-post-complex",
4+
"httpMethod": "POST",
5+
"headers": {
6+
"Accept": "*/*",
7+
"Accept-Encoding": "gzip, deflate, br",
8+
"CloudFront-Forwarded-Proto": "https",
9+
"CloudFront-Is-Desktop-Viewer": "true",
10+
"CloudFront-Is-Mobile-Viewer": "false",
11+
"CloudFront-Is-SmartTV-Viewer": "false",
12+
"CloudFront-Is-Tablet-Viewer": "false",
13+
"CloudFront-Viewer-Country": "US",
14+
"Content-Type": "application/json",
15+
"Host": "ttrvuhs0tf.execute-api.us-west-2.amazonaws.com",
16+
"Postman-Token": "cd27ded3-67a6-4d10-8deb-c3767b0f4e12",
17+
"User-Agent": "PostmanRuntime/7.28.4",
18+
"Via": "1.1 34f8ef0e4c880df0650a814412a26ea6.cloudfront.net (CloudFront)",
19+
"X-Amz-Cf-Id": "_5JSGn-BbiG9tyOsheuRSYUdQ5RZht0hu_-CcM7or8fBFv8J_rc_Ug==",
20+
"X-Amzn-Trace-Id": "Root=1-622701c5-7656138c73e95d8d237e44d7",
21+
"X-Forwarded-For": "50.35.66.233, 64.252.140.140",
22+
"X-Forwarded-Port": "443",
23+
"X-Forwarded-Proto": "https"
24+
},
25+
"multiValueHeaders": {
26+
"Accept": [
27+
"*/*"
28+
],
29+
"Accept-Encoding": [
30+
"gzip, deflate, br"
31+
],
32+
"CloudFront-Forwarded-Proto": [
33+
"https"
34+
],
35+
"CloudFront-Is-Desktop-Viewer": [
36+
"true"
37+
],
38+
"CloudFront-Is-Mobile-Viewer": [
39+
"false"
40+
],
41+
"CloudFront-Is-SmartTV-Viewer": [
42+
"false"
43+
],
44+
"CloudFront-Is-Tablet-Viewer": [
45+
"false"
46+
],
47+
"CloudFront-Viewer-Country": [
48+
"US"
49+
],
50+
"Content-Type": [
51+
"application/json"
52+
],
53+
"Host": [
54+
"ttrvuhs0tf.execute-api.us-west-2.amazonaws.com"
55+
],
56+
"Postman-Token": [
57+
"cd27ded3-67a6-4d10-8deb-c3767b0f4e12"
58+
],
59+
"User-Agent": [
60+
"PostmanRuntime/7.28.4"
61+
],
62+
"Via": [
63+
"1.1 34f8ef0e4c880df0650a814412a26ea6.cloudfront.net (CloudFront)"
64+
],
65+
"X-Amz-Cf-Id": [
66+
"_5JSGn-BbiG9tyOsheuRSYUdQ5RZht0hu_-CcM7or8fBFv8J_rc_Ug=="
67+
],
68+
"X-Amzn-Trace-Id": [
69+
"Root=1-622701c5-7656138c73e95d8d237e44d7"
70+
],
71+
"X-Forwarded-For": [
72+
"50.35.66.233, 64.252.140.140"
73+
],
74+
"X-Forwarded-Port": [
75+
"443"
76+
],
77+
"X-Forwarded-Proto": [
78+
"https"
79+
]
80+
},
81+
"queryStringParameters": null,
82+
"multiValueQueryStringParameters": null,
83+
"pathParameters": {
84+
"proxy": "test-post-complex"
85+
},
86+
"stageVariables": null,
87+
"requestContext": {
88+
"resourceId": "ri47io",
89+
"resourcePath": "/{proxy+}",
90+
"httpMethod": "POST",
91+
"extendedRequestId": "Op027H0XvHcFsKA=",
92+
"requestTime": "08/Mar/2022:07:12:05 +0000",
93+
"path": "/Prod/test-post-complex",
94+
"accountId": "626492997873",
95+
"protocol": "HTTP/1.1",
96+
"stage": "Prod",
97+
"domainPrefix": "ttrvuhs0tf",
98+
"requestTimeEpoch": 1646723525700,
99+
"requestId": "93432dd3-85b0-45c7-88ce-742b5d2b0550",
100+
"identity": {
101+
"cognitoIdentityPoolId": null,
102+
"accountId": null,
103+
"cognitoIdentityId": null,
104+
"caller": null,
105+
"sourceIp": "50.35.66.233",
106+
"principalOrgId": null,
107+
"accessKey": null,
108+
"cognitoAuthenticationType": null,
109+
"cognitoAuthenticationProvider": null,
110+
"userArn": null,
111+
"userAgent": "PostmanRuntime/7.28.4",
112+
"user": null
113+
},
114+
"domainName": "ttrvuhs0tf.execute-api.us-west-2.amazonaws.com",
115+
"apiId": "ttrvuhs0tf"
116+
},
117+
"body": "{\r\n \"testString\": \"string\"\r\n}",
118+
"isBase64Encoded": false
119+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Amazon.Lambda.AspNetCoreServer.Hosting.Internal;
2+
using Amazon.Lambda.AspNetCoreServer.Internal;
3+
using Amazon.Lambda.APIGatewayEvents;
4+
using Amazon.Lambda.TestUtilities;
5+
using Amazon.Lambda.Serialization.SystemTextJson;
6+
using Microsoft.AspNetCore.Hosting.Server;
7+
8+
if (args.Length != 2)
9+
{
10+
throw new Exception("Incorrect command line arguments: <request-file-path> <response-file-path>");
11+
}
12+
13+
// Grab the request we want to test with
14+
var requestFilePath = args[0];
15+
if(!File.Exists(requestFilePath))
16+
{
17+
throw new Exception($"Unable to find request path {requestFilePath}");
18+
}
19+
var requestJsonContent = File.ReadAllText(requestFilePath);
20+
21+
var lambdaSerializer = new DefaultLambdaJsonSerializer();
22+
var apiGatewayRequest = lambdaSerializer.Deserialize<APIGatewayProxyRequest>(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestJsonContent)));
23+
24+
25+
26+
var builder = WebApplication.CreateBuilder(args);
27+
28+
// Inject our own Lambda server replacing Kestrel.
29+
builder.Services.AddSingleton<IServer, LambdaServer>();
30+
31+
var app = builder.Build();
32+
33+
app.UseHttpsRedirection();
34+
35+
app.MapGet("/", () => "Welcome to running ASP.NET Core Minimal API on AWS Lambda");
36+
37+
app.MapPost("/test-post-complex", (Jane jane) =>
38+
{
39+
return Results.Ok($"works:{jane.TestString}");
40+
});
41+
42+
var source = new CancellationTokenSource();
43+
_ = app.RunAsync(source.Token);
44+
await Task.Delay(1000);
45+
46+
// Now that ASP.NET Core has started send the request into ASP.NET Core via Lambda function which will grab the LambdaServer register for IServer and forward the request in.
47+
var lambdaFunction = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(app.Services);
48+
var response = await lambdaFunction.FunctionHandlerAsync(apiGatewayRequest, new TestLambdaContext());
49+
50+
var responseFilePath = args[1];
51+
using (var outputStream = File.OpenWrite(responseFilePath))
52+
{
53+
lambdaSerializer.Serialize(response, outputStream);
54+
}
55+
56+
source.Cancel();
57+
58+
59+
public record Jane(string TestString);
60+

0 commit comments

Comments
 (0)