Asypi Documentation

Introduction

A simple, asynchronous, and flexible web framework built on System.Net.HttpListener. Pronounced “AS-eh-PIE.”

Hello World Example

using Asypi;

Server server = new Server(8000, "localhost");

server.Route(
    HttpMethod.Get,
    "/",
    () => { return "Hello World!"; },
    "text/html"
);

server.Run();

Features

  • Simple declarative API

  • Async support

  • Flexible router supporting variable parameters

  • Cached static file server supporting custom mount points

Getting Started

Asypi is available on nuget. To install the latest version, run dotnet add package Asypi.

Setup, Routing, and Responders

Setup

Asypi makes setting up a server almost effortless. For example, to set up and run a server listening on localhost:8000, one can use the following:

using Asypi;

Server server = new Server(8000, "localhost");

server.Run();

More constructors for Server are available in the API reference.

Server.Run() will block the main thread until the server terminates. If you need to do other things while the server runs, consider Server.RunAsync(). For example:

using Asypi;

Server server = new Server(8000, "localhost");

server.RunAsync();
DoOtherThings();

Beware that when the main thread terminates, the server does too.

Routing

Asypi makes setting up routes a simple and efficient process.

Simple Routes

using Asypi;

Server server = new Server(8000, "localhost");

// Respond to GET requests to / with body "Hello World!" and content type flag "text/html"
server.Route(
    HttpMethod.Get,
    "/",
    () => { return "Hello World!"; },
    "text/html"
);

server.Run();

Parameterized Routes

Variable parameter names must be surrounded by curly braces.

using Asypi;

Server server = new Server(8000, "localhost");

// Respond to GET requests to /{name} with a personalized greeting
server.Route(
    HttpMethod.Get,
    "/{name}",
    (Req req, Res res) => { 
        return String.Format(
            "Hello {0}!",
            req.Args[0]
        );
    },
    "text/html"
);

/*

Expected Results:
/Joe -> "Hello Joe!"
/Zoe -> "Hello Zoe!"
/123 -> "Hello 123!"

*/

server.Run();

Responders

In Asypi, each route is assigned a responder that fires whenever the route is accessed. A basic responder has the following signature and returns void:

(Req req, Res res) => {
    // do something
}

For convenience, a number of other responder signatures are supported out of the box:

() => {
    // do something
    return bodyText;
}

(Req req, Res res) => {
    // do something
    return bodyText;
}

When attaching any of these to a route, a content type must also be provided since the responder cannot reasonably infer one.

Serving Static Files

Asypi comes with both powerful utilities for routing static files and a versatile FileServer for finer control.

Serving Single Files

using Asypi;

Server server = new Server(8000, "localhost");

// Serve content.txt at /
server.RouteStaticFile("/", "content.txt");

server.Run();

Serving Directories

using Asypi;

Server server = new Server(8000, "localhost");

// Serve files from ./static at /staticfiles
server.RouteStaticDir("/staticfiles", "./static");

server.Run();

Cache-Control

When using Asypi.RouteStaticFile() or Asypi.RouteStaticDir(), the Cache-Control header is automatically set to the following:

Cache-Control: public, max-age=86400

File Compression

With a directory structure such as the following:

static
+---foo.txt
+---foo.txt.br
+---foo.txt.gz

Asypi.RouteStaticFile() and Asypi.RouteStaticDir() will first try to satisfy requests to foo.txt with foo.txt.br, followed by foo.txt.gz. Asypi automatically checks the Accept-Encoding header and automatically sets the Content-Encoding header to ensure compatibility.

Using FileServer

FileServer allows quick, easy, and cached access to files on disk. Server.RouteStaticFile() and Server.RouteStaticDir() both utilize FileServer under the hood.

FileServer caches files under an LFU system. The maximum size of the cache and the interval between cache updates can be configured when creating a Server. Note that FileServer will not cache files larger than 50% of the maximum cache size.

using Asypi;

Server server = new Server(8000, "localhost");

server.Route(
    HttpMethod.Get,
    "/",
    (Req req, Res res) => {
        byte[] content = FileServer.Get("content.txt");
        string decodedContent = Encoding.UTF8.GetString(content);
        // do something
    }
);

server.Run();

Middleware

Asypi supports a versatile middleware system, allowing one to view, modify, or cancel request/response pairs before a responder ever sees them.

To understand how middleware in Asypi works, first the “response chain” must be understood. When Asypi receives an HTTP request, a request/response pair is created and then sent as followed: server -> router -> middleware -> responder.

Middleware has the power to “break” the response chain, preventing successive middleware or the responder from operating on the request/response pair.

Writing Middleware

In Asypi, middleware is simply a function with the following signature:

(Req req, Res res) => {
    // do something
    return shouldContinue;
}

If a middleware function returns false, it will “break the response chain,” preventing successive middleware or the responder from operating on the request/response pair. If a middleware function returns true, the request/response pair will be passed down the chain normally.

Using Middleware

Middleware can be used with a simple call to Server.Use(). For example, to register doStuff() as middleware for all routes, one would call Server.Use(".*", doStuff);.

Middleware Example

// on all routes that start with private/
server.use(@"private\/.*", (Req req, Res res) => {
    // don't load the page if user is not authenticated
    if (isAuthenticated(req)) {
        return true;
    } else {
        return false;
    }
});

API Reference

Server

The Server class is the heart of Asypi. Servers handle receiving requests and sending responses, routing requests, managing routes and responders, and logging diagnostic information.

Due to the way that Server interfaces with the static FileServer, only one Server may be created per run.

Public Fields

int Port

The port that the Server will bind to when Server.Run() or Server.RunAsync() is called.


IEnumerable Hosts

The hosts that the Server will bind to when Server.Run() or Server.RunAsync() is called.


LogLevel LogLevel

The LogLevel that the Server initialized the logger with.


int LFUCacheSize

The LFU cache size, in MiB, that the Server initialized FileServer with.


int FileServerEpochLength

The epoch length, in milliseconds, that the Server initialized FileServer with.


Responder Responder404

The Responder that the Server will route requests to when no other valid routes were found.

Constructors

public Server(
    int port = 8000,
    string hostname = "localhost",
    LogLevel logLevel = LogLevel.Debug,
    int? LFUCacheSize = null,
    int? fileServerEpochLength = null
)
public Server(
    int port,
    string[] hostnames,
    LogLevel logLevel = LogLevel.Debug,
    int? LFUCacheSize = null,
    int? fileServerEpochLength = null
)

Note that LFUCacheSize is measured in MiB, and fileServerEpochLength is measured in milliseconds.

If given as null, LFUCacheSize and fileServerEpochLength will be set to default values under the hood.

Routing Methods

Routes requests to path of method method to responder.

Paths should not contain trailing slashes.

Paths can include variable parameters, the values of which will be forwarded to the responder. Variable parameters must be surrounded by curly braces. For example, /users/{id} would match /users/1, /users/joe, etc., and the responder would receive argument lists ["1"] and ["joe"] respectively.

void Route(HttpMethod method, string path, Responder responder)

void Route(HttpMethod method, string path, SimpleTextResponder responder, string contentType, IHeaders headers = null)

void Route(HttpMethod method, string path, ComplexTextResponder responder, string contentType, IHeaders headers = null)

Static File Routing

void RouteStaticFile(string path, string filePath, string contentType = null)

Routes requests to path to the file at filePath. The response will have its content type set to contentType if provided. Otherwise, the content type will be guessed.

Note that DefaultHeaders contains X-Content-Type-Options: nosniff.


RouteStaticDir(string mountRoot, string dirRoot, int? maxDepth = null, string match = ".*")

Routes requests to paths matching match under mountRoot to files under dirRoot. Content types will be guessed.

If maxDepth is not set, will recursively include all subdirectories of dirRoot. Otherwise, will only include subdirectories to a depth of maxDepth. For example, with maxDepth set to 1, will only include files directly under dirRoot.

Note that DefaultHeaders contains X-Content-Type-Options: nosniff.

If finer control is necessary, consider mounting individual files using Server.RouteStaticFile().

404 Responder Setters

void Set404Responder(Responder responder)

void Set404Responder(SimpleTextResponder responder, string contentType)

void Set404Responder(ComplexTextResponder responder, string contentType)

Other Methods

void Use(string matchExpression, Middleware middleware)

Registers the middleware for use on all paths matching matchExpression.

void Reset()

Resets the server. This will remove all previously registered routes.

Exists primarily for testing purposes.


Task RunAsync()

Runs the server. For a sync wrapper, consider Server.Run().


void Run()

Runs the server. This will block the thread until the server stops running. For async, consider Server.RunAsync().



Responders

Under the hood, all requests in Asypi are handled by a responder with the following delegate:

public delegate void Responder(
    Req request,
    Res response
);

However, for convenience, simplified responders are also generally accepted. These are:

public delegate string SimpleTextResponder();
public delegate string ComplexTextResponder(Req req, Res res);

Under the hood, each of these are wrapped by a Responder that sets the body of the Res to the string output of the simplified responder, sets content type to a separately provided value, and sets headers.



HttpMethod

public enum HttpMethod {
    Get,
    Head,
    Post,
    Put,
    Delete,
    Patch
}

Asypi also contains a few convenience items for working with HttpMethods:

string.ToHttpMethod()

HttpMethod.AsString()



Req

Req is a wrapper over System.Net.HttpListenerRequest, exposing relevant fields.

Public Fields

string BodyText

Gets the body of the request, as a string.


string BodyBytes

Gets the body of the request, as a byte[].

List<string> args

The values of applicable variable parameters.

For example, if a route was registered with the path /{name}, and a user requested /joe, the args in the resulting Req will contain ["joe"].

Other Public Fields

The majority of the fields in System.Net.HttpListenerRequest are directly wrapped by Req. For more information on these fields, refer to Microsoft’s official documentation.



Res

Res is a wrapper over System.Net.HttpListenerResponse, exposing relevant fields.

Public Fields

string BodyText

Sets the body of the response, as a string.


string BodyBytes

Sets the body of the response, as a byte[].

Other Public Fields

The majority of the fields in System.Net.HttpListenerResponse are directly wrapped by Res. For more information on these fields, refer to Microsoft’s official documentation.



IHeaders

public interface IHeaders {
    Dictionary<string, string> Values { get; }
}


DefaultHeaders

DefaultHeaders is an inheritable class implementing IHeaders containing sensible defaults for headers. To be precise, DefaultHeaders contains the following:

Server: Asypi
X-Content-Type-Options: nosniff
"X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: script-src 'self'; object-src 'none'; require-trusted-types-for 'script'

Note that DefaultHeaders does NOT contain Strict-Transport-Security, to ensure that Asypi projects work in development. Consider inheriting DefaultHeaders and adding this header.



DefaultServerHeaders

DefaultServerHeaders is a static class with a single public member, Instance. When no headers are specified for a special responder (e.g. when using Server.RouteStaticFile()), DefaultServerHeaders.Instance will be loaded by the server.

Public Fields

static IHeaders Instance

The internal instance of DefaultServerHeaders.



FileServer

FileServer is a static class provided by Asypi. It utilizes an LFU cache, which is set up when Server is initialized.

Public Methods

byte[] Get(string filePath)

Gets the content of the file at filePath as a byte[], and then updates the LFU cache.

byte[] Read(string filePath)

Gets the content of the file at filePath as a byte[], but does NOT update the LFU cache.