Middleware
ASP.NET Core middleware
Install the ASP.NET Core companion package:
dotnet add package Kinetq.LiquidPages.AspNetCore
Register LiquidPages services, initialize startup registrations, and map LiquidPages endpoints:
using Kinetq.LiquidPages.AspNetCore;
using Kinetq.LiquidPages.Helpers;
using Kinetq.LiquidPages.Interfaces;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLiquidPages(typeof(Program).Assembly);
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var startup = scope.ServiceProvider.GetRequiredService<ILiquidStartup>();
await startup.RegisterPageModels();
await startup.RegisterFilters();
string workingDirectory = Directory.GetCurrentDirectory();
startup.RegisterFileProvider("/", new PhysicalFileProvider(workingDirectory));
}
app.UseLiquidPagesErrorHandling();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapLiquidPages();
});
await app.RunAsync();
GenHTTP middleware
Install the GenHTTP companion package:
dotnet add package Kinetq.LiquidPages.GenHTTP
Resolve ILiquidResponseMiddleware and ILiquidRoutesManager from your container and pass both to LiquidHandlerBuilder:
var middleware = serviceProvider.GetRequiredService<ILiquidResponseMiddleware>();
var routesManager = serviceProvider.GetRequiredService<ILiquidRoutesManager>();
await Host.Create()
.Handler(new LiquidHandlerBuilder(middleware, routesManager))
.Bind(IPAddress.Any, 8080)
.RunAsync();
LiquidHandlerBuilder implements IHandlerBuilder<LiquidHandlerBuilder>, so you can attach any GenHTTP concern (compression, caching, CORS, etc.) before the handler is built. See the full GenHTTP documentation for a complete walkthrough.
SimpleW middleware
Install the SimpleW companion package:
dotnet add package Kinetq.LiquidPages.SimpleW
Resolve ILiquidRoutesManager, ILiquidResponseMiddleware, and ILiquidStartup from your container, then register page models and file providers before attaching LiquidPagesModule:
using Kinetq.LiquidPages.Helpers;
using Kinetq.LiquidPages.Interfaces;
using Microsoft.Extensions.FileProviders;
using SimpleW;
var liquidRoutesManager = serviceProvider.GetRequiredService<ILiquidRoutesManager>();
var liquidResponseMiddleware = serviceProvider.GetRequiredService<ILiquidResponseMiddleware>();
var liquidStartup = serviceProvider.GetRequiredService<ILiquidStartup>();
await liquidStartup.RegisterPageModels();
await liquidStartup.RegisterFilters();
liquidStartup.RegisterFileProvider("/", new PhysicalFileProvider(Directory.GetCurrentDirectory()));
var server = new SimpleWServer(IPAddress.Any, 2015);
server.UseModule(new LiquidPagesModule(liquidRoutesManager, liquidResponseMiddleware)
{
MapFallback404 = true
});
await server.RunAsync();
EmbedIO middleware
Install the EmbedIO companion package and attach LiquidPages to your WebServer:
dotnet add package Kinetq.LiquidPages.EmbedIO
var middleware = serviceProvider.GetRequiredService<ILiquidResponseMiddleware>();
var routesManager = serviceProvider.GetRequiredService<ILiquidRoutesManager>();
webServer.WithLiquidPages(middleware, routesManager);
If you need lower-level control, LiquidWebModule now takes ILiquidRoutesManager in its constructor:
webServer.WithModule(new LiquidWebModule("/", routesManager)
{
LiquidResponseMiddleware = middleware
});
Custom Middleware
If there is no existing middleware for your web server, you can implement one yourself. The middleware pipeline has been updated so that each middleware is responsible for constructing a ResponseBuilder for the underlying web server's response object and passing it into LiquidResponseMiddleware.HandleRequestAsync alongside the LiquidRequestModel.
The ResponseBuilder abstracts the mechanics of writing to a native HTTP response (status codes, content type, headers, cookies, and the body stream) so that LiquidResponseMiddleware can drive the response in a web-server-agnostic way.
The LiquidResponseBuilder<T> base class
All response builders derive from the generic abstract base class LiquidResponseBuilder<T>, where T is the underlying web server's response type. The base class stores the native response and an optional TextWriter used for streaming body output, and declares the members that concrete builders must implement:
public abstract class LiquidResponseBuilder<T>(T response, TextWriter? bodyWriter) : ILiquidResponseBuilder
{
protected readonly T Response = response;
public TextWriter? BodyWriter { get; } = bodyWriter;
public abstract void SetStatusCode(int statusCode, string? message = null);
public abstract void SetContentType(string contentType);
public abstract void AddHeader(string key, string value);
public abstract void RemoveHeader(string key);
public abstract void AddCookie(string key, string value, LiquidCookieOptions? cookieOptions = null);
public abstract void RemoveCookie(string key);
public abstract Task StartResponse();
}
Example: implementing a ResponseBuilder
Below is an example implementation of a response builder for the SimpleW web server. It wraps SimpleW's native HttpResponse object and translates each abstract member into the equivalent SimpleW API call:
public class SimpleWLiquidResponseBuilder(HttpResponse response, TextWriter? bodyWriter)
: LiquidResponseBuilder<HttpResponse>(response, bodyWriter)
{
public override void SetStatusCode(int statusCode, string? message = null)
{
Response.Status(statusCode, message);
}
public override void SetContentType(string contentType)
{
Response.ContentType(contentType);
}
public override void AddHeader(string key, string value)
{
Response.AddHeader(key, value);
}
public override void RemoveHeader(string key)
{
Response.AddHeader(key, string.Empty);
}
public override void AddCookie(string key, string value, LiquidCookieOptions? cookieOptions = null)
{
if (cookieOptions != null)
{
var sameSite = cookieOptions.SameSite switch
{
LiquidSameSiteMode.Unspecified => SameSiteMode.Unspecified,
LiquidSameSiteMode.Lax => SameSiteMode.Lax,
LiquidSameSiteMode.Strict => SameSiteMode.Strict,
_ => SameSiteMode.None
};
var options =
new HttpResponse.CookieOptions(
path: cookieOptions.Path,
domain: cookieOptions.Domain,
maxAgeSeconds: cookieOptions.MaxAge?.Seconds,
expires: cookieOptions.Expires,
secure: cookieOptions.Secure,
httpOnly: cookieOptions.HttpOnly,
sameSite: sameSite
);
Response.SetCookie(key, value, options);
}
else
{
Response.SetCookie(key, value);
}
}
public override void RemoveCookie(string key)
{
Response.DeleteCookie(key);
}
public override Task StartResponse()
{
return Task.CompletedTask;
}
}
Wiring it into your middleware
Once you have a ResponseBuilder for your web server, your custom middleware just needs to:
- Build a
LiquidRequestModelfrom the incoming request. - Construct an instance of your
ResponseBuilder, passing in the native response object and (optionally) aTextWriterfor the body. - Call the updated
LiquidResponseMiddleware.HandleRequestAsync(LiquidRequestModel, ILiquidResponseBuilder)overload, which now takes both the request model and the response builder.
try
{
var liquidRequest = new LiquidRequestModel()
{
Route = request.Url.AbsolutePath,
QueryParams = request.Url.Query.GetQueryParams(),
Headers = request.Headers
};
if (request.HasEntityBody)
{
using var reader = new StreamReader(request.InputStream, Encoding.UTF8);
liquidRequest.Body = await reader.ReadToEndAsync();
}
// Construct a ResponseBuilder for this request's native response object.
// Pass in an optional TextWriter if you want to stream the body directly
// to the underlying response stream.
using var bodyWriter = new StreamWriter(response.OutputStream, Encoding.UTF8);
var responseBuilder = new SimpleWLiquidResponseBuilder(response, bodyWriter);
// The new HandleRequestAsync signature accepts both the LiquidRequestModel
// and the custom ResponseBuilder. The middleware drives the response
// entirely through the builder — status code, content type, headers,
// cookies, and body writing are all delegated to your ResponseBuilder.
await LiquidResponseMiddleware.HandleRequestAsync(liquidRequest, responseBuilder);
}
catch (Exception ex)
{
response.StatusCode = 500;
byte[] errorBuffer = Encoding.UTF8.GetBytes($"Internal Server Error: {ex.Message}");
response.ContentLength64 = errorBuffer.Length;
response.ContentType = "text/html";
await response.OutputStream.WriteAsync(errorBuffer);
}
finally
{
response.Close();
}
Note: Each middleware must construct its own
ResponseBuilderinstance per request.ResponseBuilderinstances wrap a specific native response object and are not safe to share across requests.
Liquid Response Middleware
The ILiquidResponseMiddleware is the engine that ties everything together. After injecting it into your application, you call HandleRequestAsync with a LiquidRequestModel that encapsulates the incoming request and an ILiquidResponseBuilder that wraps the native response object. The middleware then orchestrates route matching, data retrieval, template parsing, and response generation, writing the result out through the supplied ResponseBuilder.
ILiquidResponseMiddlewareis the core service; itsHandleRequestAsyncmethod accepts aLiquidRequestModelcontaining the route, query parameters, body, and headers, together with anILiquidResponseBuilderused to write the response back to the client.The middleware iterates through all registered routes, matching the request path against each
RoutePattern(regex).When a match is found, it optionally invokes the route's
Executedelegate to obtain a view model.The middleware then parses the Liquid template using the Fluid engine, passing the view model (accessible as
view_model) and any custom filters registered in the system.Status codes, content types, headers, cookies, and body content are applied to the response through the
ResponseBuilderabstraction rather than by returning a materialized response model.If no route matches, it attempts to serve static files (e.g., CSS, images) from the default file provider.
If neither a route nor a static file is found, it falls back to any configured error routes (such as the 404 route described above) to produce an appropriate response.
This design makes the middleware adaptable to any web server — you simply provide a
LiquidResponseBuilder<T>implementation for your server's native response type, as demonstrated in the custom middleware example above. For a detailed look at the implementation, see theLiquidResponseMiddleware.csfile in the repository.
