Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
25c0c33
Support API Gateway websocket api via LambdaServer
wiowou Sep 5, 2024
1f5717a
Merge pull request #2229 from aws/dev
philasmar Dec 15, 2025
ad0ebab
update permissions (#2231) (#2232)
GarrettBeatty Dec 16, 2025
1dbfbf2
Merge branch 'dev'
aws-sdk-dotnet-automation Dec 17, 2025
a4310dc
Merge branch 'dev'
aws-sdk-dotnet-automation Jan 8, 2026
0548eb3
Dev (#2248)
GarrettBeatty Jan 9, 2026
3f6de4a
Merge branch 'dev'
aws-sdk-dotnet-automation Jan 12, 2026
fd18ef2
Merge pull request #2258 from aws/dev
philasmar Jan 14, 2026
6e7c60b
Merge branch 'dev'
aws-sdk-dotnet-automation Jan 15, 2026
15145bf
Merge branch 'dev'
aws-sdk-dotnet-automation Jan 16, 2026
bc0c745
Merge branch 'dev'
aws-sdk-dotnet-automation Jan 30, 2026
281133d
Merge branch 'dev'
aws-sdk-dotnet-automation Feb 4, 2026
e61e9a5
Merge branch 'dev'
aws-sdk-dotnet-automation Feb 10, 2026
3af4abf
Merge branch 'dev'
aws-sdk-dotnet-automation Feb 19, 2026
c28fcfa
Merge branch 'dev'
aws-sdk-dotnet-automation Feb 19, 2026
f3cf8cc
Merge pull request #2296 from aws/dev
philasmar Mar 12, 2026
dc91547
Merge pull request #2307 from aws/dev
philasmar Mar 18, 2026
a5b74f6
Merge branch 'dev'
aws-sdk-dotnet-automation Mar 19, 2026
52d0de4
Merge branch 'dev'
aws-sdk-dotnet-automation Mar 27, 2026
cb78042
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 9, 2026
64a47bb
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 14, 2026
e9d0d74
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 14, 2026
caea55b
Merge pull request #2334 from aws/dev
philasmar Apr 15, 2026
5820339
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 16, 2026
ffa4585
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 22, 2026
35b2e8f
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 22, 2026
c63d743
Merge branch 'dev'
aws-sdk-dotnet-automation Apr 29, 2026
dfbb7ac
Merge branch 'dev'
aws-sdk-dotnet-automation May 7, 2026
5de51e0
Merge branch 'dev'
aws-sdk-dotnet-automation May 12, 2026
40cdd7f
pr fixes: api gateway websocket api
May 13, 2026
4c22317
fixed merge conflict. Logical changes remain
May 13, 2026
0fc8136
fix lambda runtime support server
May 13, 2026
5ad44ab
simplify calls
May 13, 2026
acd15d7
pr fixes: api gateway websocket api
May 13, 2026
89a6b33
Merge branch 'dev' into websocket
GarrettBeatty May 15, 2026
26b9cd0
Fix websocket Hosting wiring and simplify MinimalApi
GarrettBeatty May 15, 2026
05ae11c
Add autover change file for websocket support
GarrettBeatty May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .autover/changes/apigateway-websocket-aspnetcore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.AspNetCoreServer",
"Type": "Minor",
"ChangelogMessages": [
"Add APIGatewayWebsocketApiProxyFunction (and TStartup variant) so API Gateway WebSocket APIs can be hosted via LambdaServer (DI, controllers). WebSocket events are dispatched as POST requests whose path is the RouteKey, allowing controller actions like [HttpPost(\"$default\")] to handle them.",
"Expose ParseHttpPath, ParseHttpMethod, and AddMissingRequestHeaders as protected virtual hooks on APIGatewayProxyFunction so subclasses can customize how the API Gateway request is mapped onto the ASP.NET Core request feature."
]
},
{
"Name": "Amazon.Lambda.AspNetCoreServer.Hosting",
"Type": "Minor",
"ChangelogMessages": [
"Add LambdaEventSource.WebsocketApi so AddAWSLambdaHosting can wire ASP.NET Core minimal APIs and controllers up to API Gateway WebSocket events."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,70 @@ protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCore
}
}
}

/// <summary>
/// IServer for handling Lambda events from an API Gateway Websocket API.
/// </summary>
Comment thread
wiowou marked this conversation as resolved.
public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : APIGatewayRestApiLambdaRuntimeSupportServer
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}

/// <summary>
/// Creates HandlerWrapper for processing events from API Gateway Websocket API
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider)
{
var handler = new APIGatewayWebsocketMinimalApi(serviceProvider);
#pragma warning disable CA2252
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var hostingOptions = serviceProvider.GetService<HostingOptions>();
handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false;
#pragma warning restore CA2252
Func<APIGatewayEvents.APIGatewayProxyRequest, ILambdaContext, Task<APIGatewayEvents.APIGatewayProxyResponse>> bufferedHandler = handler.FunctionHandlerAsync;
return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer);
}

/// <summary>
/// MinimalApi variant of APIGatewayWebsocketApiProxyFunction. Reuses the REST API MinimalApi plumbing
/// (snapshot collectors, hosting options, PostMarshall callbacks) and applies the websocket-specific
/// request transformations on top.
/// </summary>
public class APIGatewayWebsocketMinimalApi : APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public APIGatewayWebsocketMinimalApi(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}

/// <inheritdoc/>
protected override string ParseHttpPath(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest)
=> "/" + System.Net.WebUtility.UrlDecode(apiGatewayRequest.RequestContext.RouteKey ?? string.Empty);

/// <inheritdoc/>
protected override string ParseHttpMethod(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest) => "POST";

/// <inheritdoc/>
protected override Microsoft.AspNetCore.Http.IHeaderDictionary AddMissingRequestHeaders(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest, Microsoft.AspNetCore.Http.IHeaderDictionary headers)
{
headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers);
if (!headers.ContainsKey("Content-Type"))
{
headers["Content-Type"] = "application/json";
}
return headers;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ public enum LambdaEventSource
/// <summary>
/// ELB Application Load Balancer
/// </summary>
ApplicationLoadBalancer
ApplicationLoadBalancer,

/// <summary>
/// API Gateway WebSocket API
/// </summary>
WebsocketApi
}

/// <summary>
Expand Down Expand Up @@ -167,6 +172,7 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc
LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer),
LambdaEventSource.RestApi => typeof(APIGatewayRestApiLambdaRuntimeSupportServer),
LambdaEventSource.ApplicationLoadBalancer => typeof(ApplicationLoadBalancerLambdaRuntimeSupportServer),
LambdaEventSource.WebsocketApi => typeof(APIGatewayWebsocketApiLambdaRuntimeSupportServer),
_ => throw new ArgumentException($"Event source type {eventSource} unknown")
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Claims;
using System.Text;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -194,33 +195,9 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy
{
var requestFeatures = (IHttpRequestFeature)features;
requestFeatures.Scheme = "https";
requestFeatures.Method = apiGatewayRequest.HttpMethod;
requestFeatures.Method = this.ParseHttpMethod(apiGatewayRequest);

string path = null;

// Replaces {proxy+} in path, if exists
if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) &&
!string.IsNullOrEmpty(apiGatewayRequest.Resource))
{
var proxyPath = proxy;
path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath);

// Adds all the rest of non greedy parameters in apiGateway.Resource to the path
foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy"))
{
path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value);
}
}

if (string.IsNullOrEmpty(path))
{
path = apiGatewayRequest.Path;
}

if (!path.StartsWith("/"))
{
path = "/" + path;
}
string path = this.ParseHttpPath(apiGatewayRequest);

var rawQueryString = Utilities.CreateQueryStringParameters(
apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true);
Expand Down Expand Up @@ -257,13 +234,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy

Utilities.SetHeadersCollection(requestFeatures.Headers, apiGatewayRequest.Headers, apiGatewayRequest.MultiValueHeaders);

if (!requestFeatures.Headers.ContainsKey("Host"))
{
var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? "";
var stage = apiGatewayRequest?.RequestContext?.Stage ?? "";

requestFeatures.Headers["Host"] = $"apigateway-{apiId}-{stage}";
}
requestFeatures.Headers = this.AddMissingRequestHeaders(apiGatewayRequest, requestFeatures.Headers);


if (!string.IsNullOrEmpty(apiGatewayRequest.Body))
Expand Down Expand Up @@ -299,6 +270,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy
{
connectionFeatures.RemotePort = int.Parse(forwardedPort, CultureInfo.InvariantCulture);
}
connectionFeatures.ConnectionId = apiGatewayRequest.RequestContext?.ConnectionId;

// Call consumers customize method in case they want to change how API Gateway's request
// was marshalled into ASP.NET Core request.
Expand Down Expand Up @@ -380,5 +352,67 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature

return response;
}

/// <summary>
/// Determines the path that should be assigned to <see cref="IHttpRequestFeature.Path"/> for this request.
/// The default implementation honors <c>{proxy+}</c> resource templates and falls back to <see cref="APIGatewayProxyRequest.Path"/>.
/// Subclasses can override to derive the path from a different source (e.g. websocket route keys).
/// </summary>
protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
{
string path = null;

// Replaces {proxy+} in path, if exists
if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) &&
!string.IsNullOrEmpty(apiGatewayRequest.Resource))
{
var proxyPath = proxy;
path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath);

// Adds all the rest of non greedy parameters in apiGateway.Resource to the path
foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy"))
{
path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value);
}
}

if (string.IsNullOrEmpty(path))
{
path = apiGatewayRequest.Path;
}

if (!path.StartsWith("/"))
{
path = "/" + path;
}
return path;
}

/// <summary>
/// Determines the HTTP method that should be assigned to <see cref="IHttpRequestFeature.Method"/>.
/// The default returns <see cref="APIGatewayProxyRequest.HttpMethod"/>; subclasses can override to force
/// a fixed method (e.g. websocket events are always exposed as POST).
/// </summary>
protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest)
{
return apiGatewayRequest.HttpMethod;
}

/// <summary>
/// Adds any headers that API Gateway did not include but ASP.NET Core needs in order to route or bind the request.
/// The default implementation adds a synthesized <c>Host</c> header. Subclasses can override to add additional defaults
/// (e.g. a default <c>Content-Type</c> for websocket payloads).
/// </summary>
protected virtual IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers)
{
if (!headers.ContainsKey("Host"))
{
var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? "";
var stage = apiGatewayRequest?.RequestContext?.Stage ?? "";

headers["Host"] = $"apigateway-{apiId}-{stage}";
}
return headers;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;

using Microsoft.AspNetCore.Http;

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.AspNetCoreServer.Internal;

namespace Amazon.Lambda.AspNetCoreServer
{
/// <summary>
/// Base class for ASP.NET Core Lambda functions invoked by API Gateway Websocket APIs.
///
/// Websocket events are surfaced as POST requests whose path is the RouteKey, so the same Lambda
/// should be referenced by every websocket route that has a matching ASP.NET Core controller route
/// (e.g. <c>[HttpPost("$default")]</c>, <c>[HttpPost("$connect")]</c>) for the ASP.NET Core IServer
/// to successfully dispatch requests.
/// </summary>
public abstract class APIGatewayWebsocketApiProxyFunction : APIGatewayProxyFunction
{
/// <summary>
/// Default constructor. The ASP.NET Core framework is initialized as part of construction.
/// </summary>
protected APIGatewayWebsocketApiProxyFunction()
: base()
{
}

/// <summary>
/// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request.
/// </summary>
/// <param name="startupMode">Configures when the ASP.NET Core framework is initialized.</param>
protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode)
: base(startupMode)
{
}

/// <summary>
/// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style.
/// </summary>
/// <param name="hostedServices">The service provider built by the ASP.NET Core host.</param>
protected APIGatewayWebsocketApiProxyFunction(IServiceProvider hostedServices)
: base(hostedServices)
{
}

/// <summary>
/// Maps the websocket event to a request path of <c>/{RouteKey}</c> so ASP.NET Core can dispatch to a controller
/// route declared with <c>[HttpPost("{RouteKey}")]</c>.
/// </summary>
protected override string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
{
return "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey);
}

/// <summary>
/// Always returns <c>POST</c> for websocket events. Combined with <see cref="ParseHttpPath"/>, this lets the same
/// Lambda route every websocket event into an ASP.NET Core controller action.
/// </summary>
protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest)
{
return "POST";
}

/// <summary>
/// Adds a default <c>Content-Type</c> of <c>application/json</c> when API Gateway did not supply one.
/// Websocket message payloads are typically JSON, but the gateway does not set headers automatically.
/// </summary>
protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers)
{
headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers);
if (!headers.ContainsKey("Content-Type"))
{
headers["Content-Type"] = "application/json";
}
return headers;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Hosting;
using System.Diagnostics.CodeAnalysis;

namespace Amazon.Lambda.AspNetCoreServer
{
/// <summary>
/// Strongly-typed variant of <see cref="APIGatewayWebsocketApiProxyFunction"/> that wires up an ASP.NET Core Startup class.
/// The Lambda function handler should point at the inherited <c>FunctionHandlerAsync</c> method.
/// </summary>
Comment thread
wiowou marked this conversation as resolved.
/// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam>
public abstract class APIGatewayWebsocketApiProxyFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : APIGatewayWebsocketApiProxyFunction where TStartup : class
{
/// <summary>
/// Default constructor. The ASP.NET Core framework is initialized as part of construction.
/// </summary>
protected APIGatewayWebsocketApiProxyFunction()
: base()
{
}

/// <summary>
/// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request.
/// </summary>
Comment thread
GarrettBeatty marked this conversation as resolved.
/// <param name="startupMode">Configures when the ASP.NET Core framework is initialized.</param>
protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode)
: base(startupMode)
{
}

/// <inheritdoc/>
protected override void Init(IWebHostBuilder builder)
{
builder.UseStartup<TStartup>();
}
}
}
Loading
Loading