A collection of webserver related utilities.
$ dotnet add package KestrelToolboxKestrel Toolbox is a collection of tools and utilities for building web services using Kestrel.
You can install this package using Nuget/KestrelToolbox
dotnet add package KestrelToolbox
The core of this package is helping to spin up an IWebHost that will serve requests. These requests lean heavily on Routes attribute for how to service requests. These are similar to Controllers with some differences.
Some additional features:
public static class Routes
{
[JsonSerializable]
public class GetStorageBody
{
[Name("value")]
public string Value { get; set; }
}
// Parse the body as json. Return 400 on invalid body and return 412 when the request body type is not application/json.
[Route("/api/v1/storage/{id:guid}", methods: "POST")]
public static async Task GetStorage(HttpContext context, Guid id, [JsonBody] body)
{
// Do stuff here.
}
}
public static void Main()
{
var configuration = new WebHostBuilderConfiguration { HttpPort = PortNumber };
// Can either use the WebHostBuilderConfiguration or do it manually using the extension method
// IApplicationBuilder.UseRouteInfo(Routes.GetRoutes()).
configuration.AddRoutes(Routes.GetRoutes());
using var server = configuration.CreateWebHost();
await server.RunAsync();
}
Kestrel Toolbox has several parsing/serializing tools to make it easy to verify data with minimal code before parsing even completes. All of these features can be generated AOT using Roslyn or at runtime using IL code generation.
public enum HttpMethod
{
// Only the first argument to Name will be serialized. Others will be deserialized.
[Name("GET", "get")]
Get,
[Name("PUT", "put")]
Put,
[Name("DELETE", "delete")]
Delete,
}
[CommandLineSerializable]
public partial class CommandLineArgs
{
[Name("-X", "--request")]
[DefaultValue(HttpMethod.Get)]
public HttpMethod HttpMethod { get; set; }
// EmptyArgumentValue means that `--raw` with no trailing argument will become true.
[Name("--raw")]
[EmptyArgumentValue(true)]
public bool Raw { get; set; }
}
public static int Main(string[] args)
{
if (!CommandLineArgs.TryParse(args, out var commandLineArgs, out var errors)
{
Console.Error.WriteLine(errors);
return 1;
}
}
Filling out a feature that is missing from other mainstream languages, never again will you accidentally use a type when it shouldn't be.
An example of this would be making sure username and password are never intermingled. This is especially useful for string based types.
[Typedef(typeof(string))]
public partial class Username { }
[Typedef(typeof(string))]
public partial class Password { }
// Will throw if username or password are not the correct type.
public static Task Login(Username username, Password password)
{
await postgres.ExecuteQuery("SELECT * FROM users WHERE username=?", (string)username);
// ...rest of login.
}
[JsonSerializable]
public partial class PerformLoginRequest
{
[Name("username")]
public required Username Username { get; set; }
[Name("password")]
public required Password Password { get; set; }
}
public static async Task PerformLogin(HttpContext context, [JsonBody] PerformLoginRequest body)
{
// Will be a compile error because order of arguments is incorrect.
await Login(body.Password, body.Username);
}
There are a bunch of other odds and ends to help out with operating a web service.
Standard UUIDv1 to compliment Guid (which is UUIDv4).
Every web server has a few things that seem to be missing from base library that can help a lot.
Stop having to remember to rent and return from ArrayPool. This class adds the ability to use using statements.
using (var rental = new ArrayPoolRental(1024))
{
stream.Read(rental.Array);
}
CIDR addresses are pretty commonly used in web services. This class helps to alleviate building it yourself.
public bool ShouldBlock(IPAddress address)
{
var firewalledRange = IPAddressRange.Parse("192.168.0.0/16");
return firewalledRange.Contains(address);
}
Similar to MemoryStream except that it allocates memory out of MemoryPool. It does not currently auto expand though.
using var stream = new MemoryPoolStream(4096);
using (var fileStream = File.OpenRead(path))
{
fileStream.CopyTo(stream);
}
No more having to using SemaphoreSlim and simulating a mutex. This super lightweight mutex depends on an atomic int and will
spin before using a full lock to wait.
MutexSlim mutex;
using (await mutex.LockAsync())
{
// Do stuff under mutex.
}
// Or you can protect a value with it.
MutexSlim<int> mutex;
using (var locker = mutex.LockAsync())
{
var value = locker.Value;
locker.Value = value + 2;
}
Setting up a process can be difficult if you want to read the output or interact. This sets everything up and makes sure to wait appropriately. It also allows for async/await pattern while waiting for a process.
var process = new ProcessExtended("cat", new string[] { path1, path2 })
{
WorkingDirectory = localDirectory,
};
var output = new StringBuilder();
process.OnData += e => output.AppendLine(e);
var exitCode = await process.RunAsync();
if (exitCode != 0)
{
throw new Exception(output.ToString());
}
There is also a series of extension methods to help out with common web server functionality.
Parsing certificates from openssl into x509 is no longer a chore or requires the entire Bouncy Castle library. This utility will parse these files with minimal amounts of extra code on top of built-in dotnet.
// Read an RSA private key.
using RSA rsa = PEM.ReadPrivateKey(keyText));
// Generate a self signed certificate for testing.
X509Certificate2 cert = PEM.CreateSelfSignedServerCertificate("Example Company", "example.com", TimeSpan.FromDays(365));