Skip to content

Commit

Permalink
Add adapter for ASP.NET Core (#568)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Nov 28, 2024
1 parent 4594d5c commit 1502dd9
Show file tree
Hide file tree
Showing 16 changed files with 506 additions and 96 deletions.
83 changes: 83 additions & 0 deletions Adapters/AspNetCore/Adapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using GenHTTP.Adapters.AspNetCore.Mapping;

using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Modules.ClientCaching;
using GenHTTP.Modules.Compression;
using GenHTTP.Modules.ErrorHandling;
using GenHTTP.Modules.IO;

using Microsoft.AspNetCore.Builder;

namespace GenHTTP.Adapters.AspNetCore;

public static class Adapter
{

/// <summary>
/// Registers the given handler to respond to requests to the specified path.
/// </summary>
/// <param name="app">The application to add the mapping to</param>
/// <param name="path">The path to register the handler for</param>
/// <param name="handler">The handler to be registered</param>
/// <param name="companion">An object that will be informed about handled requests and any error</param>
public static void Map(this WebApplication app, string path, IHandlerBuilder handler, IServerCompanion? companion = null)
=> Map(app, path, handler.Build(), companion);

/// <summary>
/// Registers the given handler to respond to requests to the specified path.
/// </summary>
/// <param name="app">The application to add the mapping to</param>
/// <param name="path">The path to register the handler for</param>
/// <param name="handler">The handler to be registered</param>
/// <param name="companion">An object that will be informed about handled requests and any error</param>
public static void Map(this WebApplication app, string path, IHandler handler, IServerCompanion? companion = null)
=> app.Map(path + "/{*any}", async (context) => await Bridge.MapAsync(context, handler, companion: companion, registeredPath: path));

/// <summary>
/// Registers the given handler to respond to any request.
/// </summary>
/// <param name="app">The application to be configured</param>
/// <param name="handler">The handler to be registered</param>
/// <param name="server">The server instance that would like to execute requests</param>
public static void Run(this IApplicationBuilder app, IHandler handler, IServer server)
=> app.Run(async (context) => await Bridge.MapAsync(context, handler, server));

/// <summary>
/// Enables default features on the given handler. This should be used on the
/// outer-most handler only.
/// </summary>
/// <param name="builder">The handler to be configured</param>
/// <param name="errorHandling">If enabled, any exception will be catched and converted into an error response</param>
/// <param name="compression">If enabled, responses will automatically be compressed if possible</param>
/// <param name="clientCaching">If enabled, ETags are attached to any generated response and the tag is evaluated on the next request of the same resource</param>
/// <param name="rangeSupport">If enabled, clients can request ranges instead of the complete response body</param>
/// <typeparam name="T">The type of the handler builder which will be returned to allow the factory pattern</typeparam>
/// <returns>The handler builder instance to be chained</returns>
public static T Defaults<T>(this T builder, bool errorHandling = true, bool compression = true, bool clientCaching = true, bool rangeSupport = false) where T : IHandlerBuilder<T>
{
if (compression)
{
builder.Add(CompressedContent.Default());
}

if (rangeSupport)
{
builder.Add(RangeSupport.Create());
}

if (clientCaching)
{
builder.Add(ClientCache.Validation());
}

if (errorHandling)
{
builder.Add(ErrorHandler.Default());
}

return builder;
}

}
62 changes: 62 additions & 0 deletions Adapters/AspNetCore/GenHTTP.Adapters.AspNetCore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFrameworks>net8.0;net9.0</TargetFrameworks>

<LangVersion>13.0</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>

<AssemblyVersion>9.2.0.0</AssemblyVersion>
<FileVersion>9.2.0.0</FileVersion>
<Version>9.2.0</Version>

<Authors>Andreas Nägeli</Authors>
<Company/>

<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://genhttp.org/</PackageProjectUrl>

<Description>Adapter to run GenHTTP handlers within an ASP.NET Core app.</Description>
<PackageTags>ASP.NET Core Adapter GenHTTP</PackageTags>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591,CS1587,CS1572,CS1573</NoWarn>

<PackageIcon>icon.png</PackageIcon>

</PropertyGroup>

<ItemGroup>

<None Include="..\..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="..\..\Resources\icon.png" Pack="true" PackagePath="\"/>

</ItemGroup>

<ItemGroup>

<FrameworkReference Include="Microsoft.AspNetCore.App" />

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj"/>

<ProjectReference Include="..\..\Engine\Shared\GenHTTP.Engine.Shared.csproj" />

<ProjectReference Include="..\..\Modules\ClientCaching\GenHTTP.Modules.ClientCaching.csproj" />

<ProjectReference Include="..\..\Modules\Compression\GenHTTP.Modules.Compression.csproj" />

<ProjectReference Include="..\..\Modules\IO\GenHTTP.Modules.IO.csproj"/>
<ProjectReference Include="..\..\Modules\ErrorHandling\GenHTTP.Modules.ErrorHandling.csproj"/>

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>

</ItemGroup>

</Project>
110 changes: 110 additions & 0 deletions Adapters/AspNetCore/Mapping/Bridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using GenHTTP.Adapters.AspNetCore.Server;
using GenHTTP.Adapters.AspNetCore.Types;
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Protocol;
using Microsoft.AspNetCore.Http;

namespace GenHTTP.Adapters.AspNetCore.Mapping;

public static class Bridge
{

public static async ValueTask MapAsync(HttpContext context, IHandler handler, IServer? server = null, IServerCompanion? companion = null, string? registeredPath = null)
{
var actualServer = server ?? new ImplicitServer(handler, companion);

try
{
using var request = new Request(actualServer, context);

if (registeredPath != null)
{
AdvanceTo(request, registeredPath);
}

using var response = await handler.HandleAsync(request);

if (response == null)
{
context.Response.StatusCode = 404;
}
else
{
await WriteAsync(response, context);

actualServer.Companion?.OnRequestHandled(request, response);
}
}
catch (Exception e)
{
actualServer.Companion?.OnServerError(ServerErrorScope.ServerConnection, context.Connection.RemoteIpAddress, e);
throw;
}
}

private static async ValueTask WriteAsync(IResponse response, HttpContext context)
{
var target = context.Response;

target.StatusCode = response.Status.RawStatus;

foreach (var header in response.Headers)
{
target.Headers.Append(header.Key, header.Value);
}

if (response.Modified != null)
{
target.Headers.LastModified = response.Modified.Value.ToUniversalTime().ToString("r");
}

if (response.Expires != null)
{
target.Headers.Expires = response.Expires.Value.ToUniversalTime().ToString("r");
}

if (response.HasCookies)
{
foreach (var cookie in response.Cookies)
{
if (cookie.Value.MaxAge != null)
{
target.Cookies.Append(cookie.Key, cookie.Value.Value, new()
{
MaxAge = TimeSpan.FromSeconds(cookie.Value.MaxAge.Value)
});
}
else
{
target.Cookies.Append(cookie.Key, cookie.Value.Value);
}
}
}

if (response.Content != null)
{
target.ContentLength = (long?)response.ContentLength ?? (long?)response.Content.Length;

target.ContentType = response.ContentType?.Charset != null ? $"{response.ContentType?.RawType}; charset={response.ContentType?.Charset}" : response.ContentType?.RawType;

if (response.ContentEncoding != null)
{
target.Headers.ContentEncoding = response.ContentEncoding;
}

await response.Content.WriteAsync(target.Body, 65 * 1024);
}
}

private static void AdvanceTo(Request request, string registeredPath)
{
var parts = registeredPath.Split('/', StringSplitOptions.RemoveEmptyEntries);

foreach (var _ in parts)
{
request.Target.Advance();
}
}

}
8 changes: 8 additions & 0 deletions Adapters/AspNetCore/Server/EmptyEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Adapters.AspNetCore.Server;

public class EmptyEndpoints : List<IEndPoint>, IEndPointCollection
{

}
56 changes: 56 additions & 0 deletions Adapters/AspNetCore/Server/ImplicitServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Runtime.InteropServices;

using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Adapters.AspNetCore.Server;

public sealed class ImplicitServer : IServer
{

#region Get-/Setters

public string Version => RuntimeInformation.FrameworkDescription;

public bool Running { get; }

public bool Development
{
get
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
return string.Compare(env, "Development", StringComparison.OrdinalIgnoreCase) == 0;
}
}

public IEndPointCollection EndPoints { get; }

public IServerCompanion? Companion { get; }

public IHandler Handler { get; }

#endregion

#region Initialization

public ImplicitServer(IHandler handler, IServerCompanion? companion)
{
Handler = handler;
Companion = companion;

EndPoints = new EmptyEndpoints();

Running = true;
}

#endregion

#region Functionality

public ValueTask DisposeAsync() => new();

public ValueTask StartAsync() => throw new InvalidOperationException("Server is managed by ASP.NET Core and cannot be started");

#endregion

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using GenHTTP.Api.Protocol;
using Microsoft.AspNetCore.Http;

namespace GenHTTP.Engine.Kestrel.Hosting;
namespace GenHTTP.Adapters.AspNetCore.Types;

public sealed class ClientConnection : IClientConnection
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using GenHTTP.Api.Protocol;

using Microsoft.AspNetCore.Http;

namespace GenHTTP.Engine.Kestrel.Types;
namespace GenHTTP.Adapters.AspNetCore.Types;

public sealed class Cookies : Dictionary<string, Cookie>, ICookieCollection
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using GenHTTP.Api.Protocol;
using Microsoft.AspNetCore.Http;

namespace GenHTTP.Engine.Kestrel.Types;
namespace GenHTTP.Adapters.AspNetCore.Types;

public sealed class Headers : IHeaderCollection
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using GenHTTP.Api.Protocol;
using Microsoft.AspNetCore.Http;

namespace GenHTTP.Engine.Kestrel.Types;
namespace GenHTTP.Adapters.AspNetCore.Types;

public sealed class Query : IRequestQuery
{
Expand Down
Loading

0 comments on commit 1502dd9

Please sign in to comment.