using Amazon.Lambda.AspNetCoreServer.Internal;
using Amazon.Lambda.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Extensions.Hosting;
namespace Amazon.Lambda.AspNetCoreServer
{
///
/// Base class for ASP.NET Core Lambda functions.
///
public abstract class AbstractAspNetCoreFunction
{
///
/// Key to access the ILambdaContext object from the HttpContext.Items collection.
///
public const string LAMBDA_CONTEXT = "LambdaContext";
///
/// Key to access the Lambda request object from the HttpContext.Items collection. The object
/// can be either APIGatewayProxyRequest or ApplicationLoadBalancerRequest depending on the source of the event.
///
public const string LAMBDA_REQUEST_OBJECT = "LambdaRequestObject";
}
///
/// Base class for ASP.NET Core Lambda functions.
///
///
///
public abstract class AbstractAspNetCoreFunction : AbstractAspNetCoreFunction
{
private protected IServiceProvider _hostServices;
private protected LambdaServer _server;
private protected ILogger _logger;
private protected StartupMode _startupMode;
// Defines a mapping from registered content types to the response encoding format
// which dictates what transformations should be applied before returning response content
private readonly Dictionary _responseContentEncodingForContentType = new Dictionary
{
// The complete list of registered MIME content-types can be found at:
// http://www.iana.org/assignments/media-types/media-types.xhtml
// Here we just include a few commonly used content types found in
// Web API responses and allow users to add more as needed below
["text/plain"] = ResponseContentEncoding.Default,
["text/xml"] = ResponseContentEncoding.Default,
["application/xml"] = ResponseContentEncoding.Default,
["application/json"] = ResponseContentEncoding.Default,
["text/html"] = ResponseContentEncoding.Default,
["text/css"] = ResponseContentEncoding.Default,
["text/javascript"] = ResponseContentEncoding.Default,
["text/ecmascript"] = ResponseContentEncoding.Default,
["text/markdown"] = ResponseContentEncoding.Default,
["text/csv"] = ResponseContentEncoding.Default,
["application/octet-stream"] = ResponseContentEncoding.Base64,
["image/png"] = ResponseContentEncoding.Base64,
["image/gif"] = ResponseContentEncoding.Base64,
["image/jpeg"] = ResponseContentEncoding.Base64,
["image/jpg"] = ResponseContentEncoding.Base64,
["image/x-icon"] = ResponseContentEncoding.Base64,
["application/zip"] = ResponseContentEncoding.Base64,
["application/pdf"] = ResponseContentEncoding.Base64,
};
// Defines a mapping from registered content encodings to the response encoding format
// which dictates what transformations should be applied before returning response content
private readonly Dictionary _responseContentEncodingForContentEncoding = new Dictionary
{
["gzip"] = ResponseContentEncoding.Base64,
["deflate"] = ResponseContentEncoding.Base64,
["br"] = ResponseContentEncoding.Base64
};
///
/// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction.
///
protected AbstractAspNetCoreFunction()
: this(StartupMode.Constructor)
{
}
///
///
///
/// Configure when the ASP.NET Core framework will be initialized
protected AbstractAspNetCoreFunction(StartupMode startupMode)
{
_startupMode = startupMode;
if (_startupMode == StartupMode.Constructor)
{
Start();
}
}
///
///
///
///
protected AbstractAspNetCoreFunction(IServiceProvider hostedServices)
{
_hostServices = hostedServices;
_server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer;
_logger = ActivatorUtilities.CreateInstance>>(this._hostServices);
}
///
/// Defines the default treatment of response content.
///
public ResponseContentEncoding DefaultResponseContentEncoding { get; set; } = ResponseContentEncoding.Default;
///
/// Registers a mapping from a MIME content type to a .
///
///
/// The mappings in combination with the
/// setting will dictate if and how response content should be transformed before being
/// returned to the calling API Gateway instance.
///
/// The interface between the API Gateway and Lambda provides for repsonse content to
/// be returned as a UTF-8 string. In order to return binary content without incurring
/// any loss or corruption due to transformations to the UTF-8 encoding, it is necessary
/// to encode the raw response content in Base64 and to annotate the response that it is
/// Base64-encoded.
///
/// NOTE: In order to use this mechanism to return binary response content, in
/// addition to registering here any binary MIME content types that will be returned by
/// your application, it also necessary to register those same content types with the API
/// Gateway using either the console or the REST interface. Check the developer guide for
/// further information.
/// http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-configure-with-console.html
/// http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-configure-with-control-service-api.html
///
///
public void RegisterResponseContentEncodingForContentType(string contentType, ResponseContentEncoding encoding)
{
_responseContentEncodingForContentType[contentType] = encoding;
}
///
/// Registers a mapping from a asp.net content encoding to a lambda response content type to a .
///
///
/// The mappings in combination with the
/// setting will dictate if and how response content should be transformed before being
/// returned to the calling API Gateway instance.
///
/// The interface between the API Gateway and Lambda provides for repsonse content to
/// be returned as a UTF-8 string. In order to return binary content without incurring
/// any loss or corruption due to transformations to the UTF-8 encoding, it is necessary
/// to encode the raw response content in Base64 and to annotate the response that it is
/// Base64-encoded.
///
/// NOTE: In order to use this mechanism to return binary response content, in
/// addition to registering here any binary MIME content types that will be returned by
/// your application, it also necessary to register those same content types with the API
/// Gateway using either the console or the REST interface. Check the developer guide for
/// further information.
/// http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-configure-with-console.html
/// http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-configure-with-control-service-api.html
///
///
public void RegisterResponseContentEncodingForContentEncoding(string contentEncoding, ResponseContentEncoding encoding)
{
_responseContentEncodingForContentEncoding[contentEncoding] = encoding;
}
///
/// If true, information about unhandled exceptions thrown during request processing
/// will be included in the HTTP response.
/// Defaults to false
///
public bool IncludeUnhandledExceptionDetailInResponse { get; set; }
///
/// Method to initialize the web builder before starting the web host. In a typical Web API this is similar to the main function.
/// Setting the Startup class is required in this method.
///
///
///
/// protected override void Init(IWebHostBuilder builder)
/// {
/// builder
/// .UseStartup<Startup>();
/// }
///
///
///
protected virtual void Init(IWebHostBuilder builder) { }
///
/// Creates the IWebHostBuilder similar to WebHost.CreateDefaultBuilder but replacing the registration of the Kestrel web server with a
/// registration for Lambda.
///
///
[Obsolete("Functions should migrate to CreateHostBuilder and use IHostBuilder to setup their ASP.NET Core application. In a future major version update of this library support for IWebHostBuilder will be removed for non .NET Core 2.1 Lambda functions.")]
protected virtual IWebHostBuilder CreateWebHostBuilder()
{
var builder = new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LAMBDA_TASK_ROOT")))
{
logging.AddConsole();
logging.AddDebug();
}
else
{
logging.AddLambdaLogger(hostingContext.Configuration, "Logging");
}
})
.UseDefaultServiceProvider((hostingContext, options) =>
{
options.ValidateScopes = hostingContext.HostingEnvironment.IsDevelopment();
});
Init(builder);
// Swap out Kestrel as the webserver and use our implementation of IServer
builder.UseLambdaServer();
return builder;
}
///
/// Method to initialize the host builder before starting the host. In a typical Web API this is similar to the main function.
/// Setting the Startup class is required in this method.
///
/// It is recommended to not configure the IWebHostBuilder from this method. Instead configure the IWebHostBuilder
/// in the Init(IWebHostBuilder builder) method. If you configure the IWebHostBuilder in this method the IWebHostBuilder will be
/// configured twice, here and and as part of CreateHostBuilder.
///
///
///
///
/// protected override void Init(IHostBuilder builder)
/// {
/// builder
/// .UseStartup<Startup>();
/// }
///
///
///
protected virtual void Init(IHostBuilder builder) { }
///
/// Creates the IHostBuilder similar to Host.CreateDefaultBuilder but replacing the registration of the Kestrel web server with a
/// registration for Lambda.
///
/// When overriding this method it is recommended that ConfigureWebHostLambdaDefaults should be called instead of ConfigureWebHostDefaults to ensure the IWebHostBuilder
/// has the proper services configured for running in Lambda. That includes registering Lambda instead of Kestrel as the IServer implementation
/// for processing requests.
///
///
///
protected virtual IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder()
.ConfigureWebHostLambdaDefaults(webBuilder =>
{
Init(webBuilder);
});
Init(builder);
return builder;
}
private protected bool IsStarted
{
get
{
return _server != null;
}
}
///
/// Should be called in the derived constructor
///
protected void Start()
{
// For .NET Core 3.1 and above use the IHostBuilder instead of IWebHostBuilder used in .NET Core 2.1. If the user overrode CreateWebHostBuilder
// then fallback to the original .NET Core 2.1 behavior.
if (this.GetType().GetMethod("CreateWebHostBuilder", BindingFlags.NonPublic | BindingFlags.Instance).DeclaringType.FullName.StartsWith("Amazon.Lambda.AspNetCoreServer.AbstractAspNetCoreFunction"))
{
var builder = CreateHostBuilder();
builder.ConfigureServices(services =>
{
Utilities.EnsureLambdaServerRegistered(services);
});
var host = builder.Build();
PostCreateHost(host);
host.Start();
this._hostServices = host.Services;
}
else
{
#pragma warning disable 618
var builder = CreateWebHostBuilder();
#pragma warning restore 618
var host = builder.Build();
PostCreateWebHost(host);
host.Start();
this._hostServices = host.Services;
}
_server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer;
if (_server == null)
{
throw new Exception("Failed to find the Lambda implementation for the IServer interface in the IServiceProvider for the Host. This happens if UseLambdaServer was " +
"not called when constructing the IWebHostBuilder. If CreateHostBuilder was overridden it is recommended that ConfigureWebHostLambdaDefaults should be used " +
"instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered.");
}
_logger = ActivatorUtilities.CreateInstance>>(this._hostServices);
}
///
/// Creates a context object using the field in the class.
///
/// implementation.
protected object CreateContext(IFeatureCollection features)
{
return _server.Application.CreateContext(features);
}
///
/// Gets the response content encoding for a content type.
///
///
///
public ResponseContentEncoding GetResponseContentEncodingForContentType(string contentType)
{
if (string.IsNullOrEmpty(contentType))
{
return DefaultResponseContentEncoding;
}
// ASP.NET Core will typically return content type with encoding like this "application/json; charset=utf-8"
// To find the content type in the dictionary we need to strip the encoding off.
var contentTypeWithoutEncoding = contentType.Split(';')[0].Trim();
if (_responseContentEncodingForContentType.TryGetValue(contentTypeWithoutEncoding, out var encoding))
{
return encoding;
}
return DefaultResponseContentEncoding;
}
///
/// Gets the response content encoding for a content encoding.
///
///
///
public ResponseContentEncoding GetResponseContentEncodingForContentEncoding(string contentEncoding)
{
if (string.IsNullOrEmpty(contentEncoding))
{
return DefaultResponseContentEncoding;
}
if (_responseContentEncodingForContentEncoding.TryGetValue(contentEncoding, out var encoding))
{
return encoding;
}
return DefaultResponseContentEncoding;
}
///
/// Formats an Exception into a string, including all inner exceptions.
///
/// instance.
protected string ErrorReport(Exception e)
{
var sb = new StringBuilder();
sb.AppendLine($"{e.GetType().Name}:\n{e}");
Exception inner = e;
while (inner != null)
{
// Append the messages to the StringBuilder.
sb.AppendLine($"{inner.GetType().Name}:\n{inner}");
inner = inner.InnerException;
}
return sb.ToString();
}
///
/// This method is what the Lambda function handler points to.
///
///
///
///
[LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
public virtual async Task FunctionHandlerAsync(TREQUEST request, ILambdaContext lambdaContext)
{
if (!IsStarted)
{
Start();
}
InvokeFeatures features = new InvokeFeatures();
MarshallRequest(features, request, lambdaContext);
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
var httpRequestFeature = (IHttpRequestFeature)features;
_logger.LogDebug($"ASP.NET Core Request PathBase: {httpRequestFeature.PathBase}, Path: {httpRequestFeature.Path}");
}
{
var itemFeatures = (IItemsFeature)features;
itemFeatures.Items = new ItemsDictionary();
itemFeatures.Items[LAMBDA_CONTEXT] = lambdaContext;
itemFeatures.Items[LAMBDA_REQUEST_OBJECT] = request;
PostMarshallItemsFeatureFeature(itemFeatures, request, lambdaContext);
}
var scope = this._hostServices.CreateScope();
try
{
((IServiceProvidersFeature)features).RequestServices = scope.ServiceProvider;
var context = this.CreateContext(features);
var response = await this.ProcessRequest(lambdaContext, context, features);
return response;
}
finally
{
scope.Dispose();
}
}
///
/// Processes the current request.
///
/// implementation.
/// The hosting application request context object.
/// An instance.
///
/// If specified, an unhandled exception will be rethrown for custom error handling.
/// Ensure that the error handling code calls 'this.MarshallResponse(features, 500);' after handling the error to return a the typed Lambda object to the user.
///
protected async Task ProcessRequest(ILambdaContext lambdaContext, object context, InvokeFeatures features, bool rethrowUnhandledError = false)
{
var defaultStatusCode = 200;
Exception ex = null;
try
{
try
{
await this._server.Application.ProcessRequestAsync(context);
}
catch (AggregateException agex)
{
ex = agex;
_logger.LogError(agex, $"Caught AggregateException: '{agex}'");
var sb = new StringBuilder();
foreach (var newEx in agex.InnerExceptions)
{
sb.AppendLine(this.ErrorReport(newEx));
}
_logger.LogError(sb.ToString());
((IHttpResponseFeature)features).StatusCode = 500;
}
catch (ReflectionTypeLoadException rex)
{
ex = rex;
_logger.LogError(rex, $"Caught ReflectionTypeLoadException: '{rex}'");
var sb = new StringBuilder();
foreach (var loaderException in rex.LoaderExceptions)
{
var fileNotFoundException = loaderException as FileNotFoundException;
if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName))
{
sb.AppendLine($"Missing file: {fileNotFoundException.FileName}");
}
else
{
sb.AppendLine(this.ErrorReport(loaderException));
}
}
_logger.LogError(sb.ToString());
((IHttpResponseFeature)features).StatusCode = 500;
}
catch (Exception e)
{
ex = e;
if (rethrowUnhandledError) throw;
_logger.LogError(e, $"Unknown error responding to request: {this.ErrorReport(e)}");
((IHttpResponseFeature)features).StatusCode = 500;
}
if (features.ResponseStartingEvents != null)
{
await features.ResponseStartingEvents.ExecuteAsync();
}
var response = this.MarshallResponse(features, lambdaContext, defaultStatusCode);
if (ex != null && IncludeUnhandledExceptionDetailInResponse)
{
InternalCustomResponseExceptionHandling(response, lambdaContext, ex);
}
if (features.ResponseCompletedEvents != null)
{
await features.ResponseCompletedEvents.ExecuteAsync();
}
return response;
}
finally
{
this._server.Application.DisposeContext(context, ex);
}
}
private protected virtual void InternalCustomResponseExceptionHandling(TRESPONSE lambdaReponse, ILambdaContext lambdaContext, Exception ex)
{
}
///
/// This method is called after the IWebHost is created from the IWebHostBuilder and the services have been configured. The
/// WebHost hasn't been started yet.
///
///
protected virtual void PostCreateWebHost(IWebHost webHost)
{
}
///
/// This method is called after the IHost is created from the IHostBuilder and the services have been configured. The
/// Host hasn't been started yet. If the CreateWebHostBuilder method is overloaded then IHostWebBuilder will be used to create
/// an IWebHost and this method will not be called.
///
///
protected virtual void PostCreateHost(IHost webHost)
{
}
///
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IItemsFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{
}
///
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IHttpAuthenticationFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{
}
///
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IHttpRequestFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{
}
///
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IHttpConnectionFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{
}
///
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's ITlsConnectionFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{
}
///
/// This method is called after marshalling the IHttpResponseFeature that came
/// back from making the request into ASP.NET Core into the Lamdba response object. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
///
///
///
///
protected virtual void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, TRESPONSE lambdaResponse, ILambdaContext lambdaContext)
{
}
///
/// Converts the Lambda request object into ASP.NET Core InvokeFeatures used to create the HostingApplication.Context.
///
///
///
///
protected abstract void MarshallRequest(InvokeFeatures features, TREQUEST lambdaRequest, ILambdaContext lambdaContext);
///
/// Convert the ASP.NET Core response to the Lambda response object.
///
///
///
///
///
protected abstract TRESPONSE MarshallResponse(IHttpResponseFeature responseFeatures, ILambdaContext lambdaContext, int statusCodeIfNotSet = 200);
}
}