Oddhouse.Vestibule
1.0.1
dotnet add package Oddhouse.Vestibule --version 1.0.1
NuGet\Install-Package Oddhouse.Vestibule -Version 1.0.1
<PackageReference Include="Oddhouse.Vestibule" Version="1.0.1" />
<PackageVersion Include="Oddhouse.Vestibule" Version="1.0.1" />
<PackageReference Include="Oddhouse.Vestibule" />
paket add Oddhouse.Vestibule --version 1.0.1
#r "nuget: Oddhouse.Vestibule, 1.0.1"
#:package Oddhouse.Vestibule@1.0.1
#addin nuget:?package=Oddhouse.Vestibule&version=1.0.1
#tool nuget:?package=Oddhouse.Vestibule&version=1.0.1
Oddhouse.Vestibule
A lightweight .NET 9 library for building REST servers. Define endpoints by subclassing RestServer and decorating methods with HTTP method attributes — no configuration files, no middleware pipeline, no dependency injection required.
Quick start
using Oddhouse.Net;
var server = new MyServer();
Console.ReadLine(); // keep alive
public class MyServer : RestServer
{
public MyServer() : base(8080) { }
[HttpGet("/hello/{name}")]
public string Hello(string name) => $"Hello, {name}!";
}
The server starts listening on both IPv4 and IPv6 as soon as the constructor returns. Dispose() shuts it down.
Defining endpoints
Decorate methods with an HTTP method attribute and a URI template:
[HttpGet("/users/{id}")]
public User GetUser(int id) { ... }
[HttpPost("/users")]
public User CreateUser([Body] User user) { ... }
[HttpPut("/users/{id}")]
public User UpdateUser(int id, [Body] User user) { ... }
[HttpDelete("/users/{id}")]
public void DeleteUser(int id) { ... }
Available attributes: [HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete], [HttpHead], [HttpOptions], [HttpTrace].
Use [Http] without a method to match any HTTP verb:
[Http("/ping")]
public string Ping() => "pong";
URI templates
Path parameters
Segments wrapped in {braces} are bound to method parameters by name:
[HttpGet("/orders/{year}/{month}")]
public IReadOnlyList<Order> GetOrders(int year, int month) { ... }
Query parameters
// Required query parameter
[HttpGet("/users?role={role}")]
public IReadOnlyList<User> GetByRole(string role) { ... }
// Optional query parameter (has a default value)
[HttpGet("/users?page={page}&size={size}")]
public IReadOnlyList<User> ListUsers(int page = 1, int size = 20) { ... }
Wildcard suffix
Append [/...] to match any number of additional path segments:
[HttpGet("/files/{name}[/...]")]
public Stream GetFile(string name) { ... }
Supported parameter types
All primitives (int, bool, double, …), string, Guid, DateTime, TimeSpan, enums, and their nullable equivalents.
Return types
| Return type | Effect |
|---|---|
void / Task |
Empty response body |
T / Task<T> |
Serialized into the response body |
byte[] |
Written directly to the response stream |
Stream |
Piped directly to the response stream |
Async handlers are fully supported:
[HttpGet("/users/{id}")]
public async Task<User> GetUser(int id)
{
return await db.Users.FindAsync(id);
}
Request body
Mark one parameter with [Body] to receive the deserialized request body. The serializer is chosen based on the request's Content-Type header:
[HttpPost("/users")]
public async Task<User> CreateUser([Body] User user) { ... }
Content negotiation
The response serializer is chosen by matching the request's Accept header against the server's Serializers collection. The built-in serializers cover:
| Content type | Serializer |
|---|---|
application/json |
JsonSerializer |
application/xml, text/xml |
XmlSerializer |
text/plain |
PlainTextSerializer |
DefaultContentType controls which serializer is used when the client sends no Accept header or accepts */*. It defaults to application/json.
public class MyServer : RestServer
{
public MyServer() : base(8080)
{
DefaultContentType = "application/xml";
}
}
Add or remove serializers via the Serializers collection:
Serializers.Add(new MyCustomSerializer());
Serializers.Remove("text/plain");
Force a specific content type for one endpoint using [ContentType]:
[HttpGet("/feed")]
[ContentType("application/rss+xml")]
public Stream GetFeed() { ... }
Accessing the request context
RestServer exposes static, async-local properties that give ambient access to the current request from anywhere in the call stack:
RestServer.Request // HttpRequest — URI, headers, body stream
RestServer.Response // HttpResponse — status code, headers, response stream
RestServer.Connection // HttpConnection — local/remote address, per-connection state
Set the status code directly when the default (200 OK) is not appropriate:
[HttpPost("/users")]
public User CreateUser([Body] User user)
{
var created = repository.Insert(user);
Response.StatusCode = HttpStatusCode.Created;
return created;
}
Connection events
Subscribe to ClientConnected and ClientDisconnected to track active connections or initialize per-connection state:
public MyServer() : base(8080)
{
ClientConnected += (_, connection) => Console.WriteLine($"Connected: {connection.RemoteAddress}");
ClientDisconnected += (_, connection) => Console.WriteLine($"Disconnected: {connection.RemoteAddress}");
}
Filters
Apply [ContentType] to declare which content types an endpoint produces. If the client's Accept header cannot be satisfied, 406 Not Acceptable is returned automatically:
[HttpGet("/report")]
[ContentType("application/pdf", RequireMatchingAcceptHeader = true)]
public Stream GetReport() { ... }
Implement RequestFilterAttribute to write custom filters:
public sealed class RequireApiKeyAttribute : RequestFilterAttribute
{
public override bool AllowRequest(
HttpContext context, MethodInfo method,
out HttpStatusCode? errorCode, out string? errorDescription)
{
if (context.Request.Headers["X-Api-Key"] == "secret")
{
errorCode = null;
errorDescription = null;
return true;
}
errorCode = HttpStatusCode.Unauthorized;
errorDescription = "Unauthorized";
return false;
}
}
// Usage
[HttpGet("/admin/stats")]
[RequireApiKey]
public Stats GetStats() { ... }
HTTPS
Override UriScheme and Certificate to enable TLS:
public class SecureServer : RestServer
{
public SecureServer() : base(443) { }
protected override string UriScheme => Uri.UriSchemeHttps;
protected override X509Certificate2? Certificate =>
X509Certificate2.CreateFromPemFile("cert.pem", "key.pem");
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. 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 was computed. 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. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.