Elsie.HealthChecks
0.2.0-alpha.2
dotnet add package Elsie.HealthChecks --version 0.2.0-alpha.2
NuGet\Install-Package Elsie.HealthChecks -Version 0.2.0-alpha.2
<PackageReference Include="Elsie.HealthChecks" Version="0.2.0-alpha.2" />
<PackageVersion Include="Elsie.HealthChecks" Version="0.2.0-alpha.2" />
<PackageReference Include="Elsie.HealthChecks" />
paket add Elsie.HealthChecks --version 0.2.0-alpha.2
#r "nuget: Elsie.HealthChecks, 0.2.0-alpha.2"
#:package Elsie.HealthChecks@0.2.0-alpha.2
#addin nuget:?package=Elsie.HealthChecks&version=0.2.0-alpha.2&prerelease
#tool nuget:?package=Elsie.HealthChecks&version=0.2.0-alpha.2&prerelease
Elsie
HTTP module framework for .NET 8 and .NET 10. Define routes in small modules, return results, host on ASP.NET Core.
dotnet add package Elsie # 0.2.0-alpha.2 — pulls Elsie.AspNetCore + Elsie.Core
using Elsie;
using Elsie.AspNetCore;
ElsieWeb.Run<App>(args);
public sealed class App : ElsieModule
{
public App()
{
Get("/", () => ElsieResult.Text("Hello, Elsie!"));
Get("/hello/{name}", ctx =>
ElsieResult.Text($"Hello {ctx.RouteOrDefault("name")}!"));
}
}
dotnet run
# GET / → Hello, Elsie!
# GET /hello/Ada → Hello Ada!
Templates:
dotnet new install Elsie.Templates
dotnet new elsie -n HelloApp # minimal app
dotnet new elsie-api -n TodosApi # CRUD + API key + OpenAPI
Guides: docs/ · Samples: HelloWorld · Hello · Api · Views · Full · NuGet: Elsie
Quickstart (builder)
var builder = WebApplication.CreateBuilder(args);
builder.AddElsie(); // DI + quieter console logs
builder.Services.AddElsieModule<TodosModule>(); // or rely on entry-assembly scan
var app = builder.Build();
app.MapElsieOpenApi(o =>
{
o.Info.Title = "My API";
o.UiPath = "/scalar"; // optional Scalar CDN UI
});
app.MapElsie();
app.Run();
| API | Use |
|---|---|
ElsieWeb.Run<T>(args) |
Smallest host |
builder.AddElsie() |
Wire DI (ScanEntryAssembly = true by default) |
AddElsieModule<T>() |
Explicit module registration (prefer in tests) |
app.MapElsie() |
Map Elsie into the pipeline (non-terminal by default) |
Modules are singletons. Inject singleton-safe services in the ctor; resolve request-scoped services with ctx.GetRequiredService<T>() / ctx.Services.
JSON: ElsieResult.Json(...) uses framework defaults (ElsieJson.DefaultOptions); ctx.Json(...) uses app ElsieOptions.JsonSerializerOptions.
Modules and routing
public sealed class TodosModule : ElsieModule
{
public TodosModule(ITodoStore store)
{
Path("/api");
Before(ElsieAuth.RequireApiKey("dev-secret", onlyMutatingMethods: true));
Group("/todos", () =>
{
Get("/", ctx => ctx.Json(store.List(ctx.QueryOrDefault("q"))))
.Named("listTodos")
.WithTags("todos");
Get("/{id:guid}", ctx =>
{
if (!ctx.RequireRoute("id", out Guid id, out var error))
return error!;
return ctx.Json(store.Get(id));
}).Named("getTodo").Produces<Todo>();
Post("/", async (ctx, ct) =>
{
var bind = await ctx.BindJsonAsync<CreateTodo>(ct);
if (!bind.IsSuccess) return bind.Error!;
var created = store.Add(bind.Value!.Title);
return ElsieResult.Created(ctx.UrlFor("getTodo", new { id = created.Id }), created);
}).Accepts<CreateTodo>().Produces<Todo>(201);
});
}
}
Route templates: {name}, optional {name?}, default {name=5}, constraints {id:int}, catch-all {*path} (last segment only).
Built-in constraints: int, long, guid, bool, alpha, datetime, decimal, double, minlength(n), maxlength(n), length(n|min,max), min(n), max(n), range(a,b), regex(...).
Matching: per segment, static > constrained > param > catch-all. Startup fails on unknown constraints, duplicate param names, bad catch-all placement, ambiguous routes, and duplicate route names. Wrong verb on a known path → 405 + Allow + problem+json. HEAD maps to GET when ElsieOptions.ImplicitHead is on (default).
Results, binding, context
ElsieResult.Text("ok")
ElsieResult.Html("<p>hi</p>")
ElsieResult.Json(payload, statusCode: 201) // framework JSON defaults
ctx.Json(payload) // app JsonSerializerOptions
ElsieResult.Created("/items/1", body)
ElsieResult.Accepted()
ElsieResult.File(bytes, "application/pdf", downloadName: "a.pdf")
ElsieResult.Redirect("/elsewhere") // 302; 307/308 helpers also
ElsieResult.NoContent()
ElsieResult.NotModified()
ElsieResult.Problem(400, "Bad Request", detail)
ElsieResult.ValidationProblem(errors)
ElsieResult.ServerSentEvents(async (w, ct) => await w.WriteEventAsync("tick", "1", ct))
var bind = await ctx.BindJsonAsync<CreateTodo>(ct);
var form = await ctx.BindFormAsync<LoginForm>(ct);
var query = ctx.BindQuery<SearchQuery>();
ctx.Route<int>("id");
ctx.Query<bool>("shout");
ctx.RequireRoute("id", out Guid id, out var error);
ctx.Request.Method / Path / Scheme / Host / PathBase / Protocol / RemoteIp
ctx.Request.GetHeader / GetQuery / GetCookie
ctx.UrlFor("getTodo", new { id })
ctx.Response.Headers["X-App"] = "1";
ctx.Response.SetCookie("sid", value, new ElsieCookieOptions { HttpOnly = true });
ctx.GetRequiredService<IMyService>();
Exception handling (first match wins):
builder.AddElsie(o =>
{
o.MapException<KeyNotFoundException>((_, ex) => ElsieResult.NotFound(ex.Message));
o.ExceptionHandler = (ctx, ex, ct) =>
Task.FromResult(ElsieResult.Problem(500, "Server Error", ex.Message));
});
// MapException → module OnError → ExceptionHandler → rethrow
// After-hooks still run when the exception is mapped to a result
Need HttpContext? Host escape hatch (core stays free of it):
Get("/trace", ctx =>
ctx.TryGetHttpContext(out var http)
? ElsieResult.Text(http!.TraceIdentifier)
: ElsieResult.Text("no-host"));
Pipelines
App-wide or per-module before / after hooks. After-hooks may transform the result.
builder.Services.ConfigureElsiePipelines(p =>
{
p.AddBefore((ctx, _) =>
{
ctx.Response.Headers["X-Request-Id"] = Guid.NewGuid().ToString("n");
return Task.FromResult<ElsieResult?>(null); // null = continue
});
p.AddAfter((ctx, result) =>
{
ctx.Response.Headers["X-Status"] = result.StatusCode.ToString();
return result;
});
});
// Order: app.Before → module.Before → handler → module.After → app.After
Core gates (no ASP.NET auth stack):
Before(ElsieAuth.RequireApiKey("dev-secret")); // all methods (default)
Before(ElsieAuth.RequireApiKey("dev-secret", onlyMutatingMethods: true));
Before(ElsieAuth.RequireHeader("X-Tenant", "acme"));
Before(ElsieAuth.RequireBearer(token => token == "ok"));
Before(ElsieAuth.RequireCookie("session"));
Cookie/JWT policies → Elsie.Auth.
Optional packages
Auth — Elsie.Auth
builder.Services.AddElsieAuth(o => o.Cookie = c => c.Cookie.Name = "elsie-auth");
app.UseElsieAuth(); // before MapElsie
Before(ElsieAuthGates.RequireAuthenticated());
Before(ElsieAuthGates.RequireRole("admin"));
await ctx.SignInCookieAsync("ada", roles: ["user"]);
var user = ctx.GetUser();
CORS — Elsie.Cors
Elsie handles OPTIONS preflight itself (ASP.NET UseCors does not see those requests).
builder.Services.AddElsieCors(o => o.AddDefaultPolicy(p => p
.AllowOrigins("http://localhost:5173")
.AllowMethods("GET", "POST", "OPTIONS")
.AllowHeaders("Content-Type")
.AllowCredentials()));
app.UseElsieCors(); // before MapElsie
// optional: Get(...).WithCors("policy-name")
Health — Elsie.HealthChecks
builder.Services.AddElsieHealthChecks(o =>
{
o.AddCheck("self", () => ElsieHealthCheckResult.Healthy(), ElsieHealthCheckTags.Live);
o.AddCheck("db", ct => CheckDbAsync(ct), ElsieHealthCheckTags.Ready);
});
// GET /healthz | /healthz/live | /healthz/ready (unhealthy → 503)
Rate limiting — Elsie.RateLimiting
Before(ElsieRateLimit.FixedWindow(100, TimeSpan.FromMinutes(1)));
Before(ElsieRateLimit.SlidingWindow(30, TimeSpan.FromSeconds(10),
partitionKey: ctx => ctx.Request.GetHeader("X-Api-Key") ?? "anon"));
// 429 problem+json + Retry-After; uses TimeProvider
OpenAPI (in host)
Route metadata (.Named / .Accepts / .Produces / .WithSecurity / …) builds the document.
app.MapElsieOpenApi(o =>
{
o.Info.Title = "My API";
o.Info.SecuritySchemes["ApiKey"] = ElsieOpenApiSecurityScheme.ApiKeyHeader();
o.UiPath = "/scalar";
});
Views — Elsie.Views (Fluid / Liquid)
builder.Services.AddElsieViews(o => o.ContentRoot = builder.Environment.ContentRootPath);
return await ctx.ViewAsync("home", new { Title = "Hi", Name = "Ada" }, cancellationToken: ct);
{% layout '_Layout.liquid' %}
<h1>Hello {{ Name }}!</h1>
Static files (host)
app.MapElsieStaticFiles("/assets", Path.Combine(contentRoot, "wwwroot"));
// ETag / Last-Modified + 304; no range requests
Validation — Elsie.FluentValidation
var bind = await ctx.BindAndValidateJsonAsync<CreateTodo>(ct);
Testing — Elsie.Testing
await using var mem = ElsieInMemoryHost.Create(s => s.AddElsieModule<HelloModule>());
var r = await mem.GetAsync("/hello/Ada");
Assert.Equal(200, r.StatusCode);
await using var host = ElsieTestHost.Create(s => s.AddElsieModule<HelloModule>());
var response = await host.GetAsync("/hello/Ada");
response.AssertStatus(200);
In tests, set ScanEntryAssembly = false and register modules explicitly.
Package layout
| Package | Contents |
|---|---|
| Elsie | Meta-package for apps → Elsie.AspNetCore → Elsie.Core |
| Elsie.AspNetCore | ElsieWeb, AddElsie, MapElsie, OpenAPI, static files |
| Elsie.Core | Host-agnostic modules, routing, dispatcher, results, pipelines |
| Elsie.Auth | Cookie/JWT + auth gates |
| Elsie.Cors | Elsie-native CORS |
| Elsie.HealthChecks | /healthz live/ready |
| Elsie.RateLimiting | Before-hook rate limits |
| Elsie.Views | Fluid/Liquid views |
| Elsie.FluentValidation | BindAndValidateJsonAsync |
| Elsie.Testing | In-memory + TestServer hosts |
| Elsie.Templates | dotnet new elsie / elsie-api |
Current version: 0.2.0-alpha.2 (prerelease; APIs may still change).
Namespaces stay Elsie / Elsie.AspNetCore regardless of package id. Library authors who want the host-agnostic surface only should reference Elsie.Core.
Request flow
HTTP request
→ host (ASP.NET Core or in-memory test host)
→ ElsieRequest
→ RouteTable.Lookup → before hooks → handler → after hooks → ElsieResult
→ ElsieHttpResponse.FromDispatch
→ host writes status / headers / body
MapElsie is non-terminal by default so OpenAPI, static files, and other endpoints can coexist. MapElsie(terminal: true) returns 404 problem+json for unmatched routes.
Docs
| Topic | |
|---|---|
| Getting started | Install, smallest app, samples |
| Modules | Registration, DI lifetimes |
| Routing | Templates, constraints, precedence |
| Results | Response factories |
| Binding | Route/query/JSON/form |
| Pipelines & errors | Before/after, exception maps |
| Auth | Core gates + Elsie.Auth |
| CORS | Elsie.Cors |
| Rate limiting | Fixed/sliding windows |
| Health checks | Live/ready |
| OpenAPI | Document + Scalar |
| Views | Fluid/Liquid |
| Static files | MapElsieStaticFiles |
| Testing | In-memory + TestServer |
| Hosting & AOT | Host notes |
Changelog: CHANGELOG.md
License
MIT © WavestormSoftware — LICENSE
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Elsie.Core (>= 0.2.0-alpha.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
-
net8.0
- Elsie.Core (>= 0.2.0-alpha.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|