Skip to content

Builder System

Max Brauer edited this page Feb 12, 2022 · 1 revision

This is a feature that was introduced in Version 3.3.0

The Builder System let you define new web services without much code. For this you write a little class with some methods that will listen to incoming requests, mark them with attributes and a Builder will use Reflections to create the web services for you. At runtime there are no more reflections in use and most of the stuff is running as normal (with a little overhead because of casting).

First steps

All to do is to add the following usings to your code:

using MaxLib.WebServer; // base classes for all the stuff
using MaxLib.WebServer.Builder; // here goes the fancy builder magic

Then you create a public, non-abstract, non-generic class that inherits the MaxLib.WebServer.Builder.Service class:

public class MyServices : Service

Here you can add one or more public, non-abstract, non-generic methods in mostly any kind you like. The builder system will try to fit the stuff in the kind it is needed. Keep in mind: The builder won't throw exceptions if something doesn't fit - it will just ignore your methods.

Let's add a simple "Hello World" method that listens to /hello:

[Path("/hello")]
public string Hello()
{
    return "Hello World";
}

Setup some rules

Currently there exists only one kind of rule: Path. This will check if the path matches (or starts) with the specified pattern. This will ignore GET parameters (for this is some other stuff reserved). The path is split at any / (ignoring empty paths) and checked one by one. If you want to use some part of the path as a variable you can wrap the part with a name in curly braces { and } like so:

[Path("/api/add/{a}/{b}")]

If you want to access the variables in your method body you have to mark you parameter with the Var attribute:

[Path("/api/add/{a}/{b}")]
public string ApiAdd([Var] double a, [Var] double b)
{
    return $"{a} + {b} = {a + b}";
}

More tricks:

  • You can set Path to prefix mode like so: [Path("/api", Prefix=true)]
  • You can also set the Path attribute to the class. This will add this rule to all contained methods.
  • You can use nested classes with nested Path attributes. This will extend the rules.
  • If you need to name your parameter differently you can specify the name like so: [Var("varName")] string paramName

GET parameter

You can access any GET parameter similar to the vars if you add the Get attribute to the parameter.

[Path("/api/add")]
public string ApiAddGet([Get] double a, [Get] double b)
{
    return $"{a} + {b} = {a + b}";
}

All GET parameter that are defined like so are required.

Access to the WebProgressTask and their components

It is quite easy to access any of these members. You only need to define the type and the builder system will look if it knows a method how the access it.

public string DirectAccess(WebProgressTask task)
{
    return "it works";
}

Currently the builder system knows how to access the following types:

  • MaxLib.WebServer.WebProgressTask
  • MaxLib.WebServer.HttpDocument
  • MaxLib.WebServer.Server
  • MaxLib.WebServer.HttpConnection
  • MaxLib.WebServer.Sessions.Session
  • MaxLib.WebServer.HttpRequestHeader
  • MaxLib.WebServer.HttpResponseHeader
  • MaxLib.WebServer.HttpLocation
  • MaxLib.WebServer.HttpPost
  • MaxLib.WebServer.HttpConnectionType
  • MaxLib.WebServer.HttpCookie
  • MaxLib.WebServer.Post.IPostData
  • System.Threading.Tasks.Task<MaxLib.WebServer.IPostData>
  • MaxLib.WebServer.Post.MultipartFormData
  • MaxLib.WebServer.Post.UnknownPostData
  • MaxLib.WebServer.Post.UrlEncodedData
  • System.Net.IPEndPoint (endpoint of client)
  • System.Net.IPAddress (ip of client)

Conversion of inputs

The builder systems knows some methods how to transform the [Var] and [Get] parameters. First it will check if the current value can be directly assigned (that is possible if you inherit from string) and then it will check if the type inherits System.IConvertible which is true for all .NET primitives (int, double, string, ...).

If you want to use your variable differently (e.g. as an array, different encoding, ...) you have to define your own converter. For this you need to create a class which implements the interface MaxLib.WebServer.Builder.Tools.IConverter. Here you define the GetConverter method which receives the input (from the source) and output (expected by the method parameter) types and return a function which can transform between this.

To use this converter you have to add the [Converter] attribute:

public string GetPoint([Converter(typeof(PointConverter))] [Get] Point p)
{
    return p.ToString();
}

The [Get] and [Var] attributes always provide string.

Conversion of outputs

First of all it will check if the return type fits in one of the following categories:

  1. void
  2. System.Threading.Tasks.Task<T>
  3. System.Threading.Tasks.ValueTask<T>
  4. T

In case 1 it will do nothing special after finishing your method. In case 2 and 3 it will await the completion of the task and then handle the result in the same way like in case 4.

In case 4 the result would be transformed in MaxLib.WebServer.HttpDataSource and then added to the HttpDocument (which is returned to the client). For the following cases this works fine:

  • MaxLib.WebServer.HttpDataSource or any class that derives it
  • string: will be transformed in MaxLib.WebServer.HttpStringDataSource with the mime type text/html
  • System.IO.Stream: will be transformed in MaxLib.WebServer.HttpStreamDataSource with the mime type text/html
  • System.IO.FileInfo: will be transformed in MaxLib.WebServer.HttpFileDataSource. The mime type is derived from the file extension and defaults to application/octet-stream.

If you want to define your own conversion method for different types, you need to define your own converter which inherits MaxLib.WebServer.Builder.Tools.IDataConverter. The GetConverter method receives only the returned type T (without any Tasks our such) and returns a method which can convert this into a HttpDataSource.

[return: DataConverter(typeof(PointConverter))]
public async Task<Point> GetPoint([Get] string id)
{
    return await Database.GetPoint(id);
}

Ignoring methods or classes

The builder system will ignore the methods that is derived from object. If you want to ignore you own methods or types you have to attach the [Ignore] attribute.

[Ignore]
public string GetId()
{
    return "secret";
}

Adding to the web server

It is quite easy to add your services to the web server. For this you need to convert this to a WebService. The MaxLib.WebServer.Builder.Service provides some static methods for it:

  • Service.Build<T>() builds the specific service
  • Service.Build(Type) build the specified service
  • Service.Build(Assembly) build all services in the specified assembly
  • Service.Build(AppDomain) build all services in all assemblies in the specified AppDomain
  • Service.Build build all services in all assemblies in the current AppDomain

These methods can return null if the type doesn't follow the rules or no service was found. Before adding to the server you should of course check this.

var service = Service.Build<MyServices>();
if (service != null)
    webServer.AddWebService(service);

More stuff

  1. Every method receives its own instance of the class it resides in. There is currently no way that all these methods shares the same instance.
  2. The build system won't throw exceptions and ignore invalid configurations. If you use you own extensions this behavior can change.
  3. You can throw MaxLib.WebServer.HttpException to cancel the execution. You can optionally add some status to this exception.
  4. You can change the priority of the method using the [Priotity(WebServicePriority)] attribute.
  5. You can add more rules if you inherit the MaxLib.WebServer.Builder.Tools.RuleAttributeBase class.
  6. You can add more source rules to parameter if you inherit the MaxLib.WebServer.Builder.Tools.ParamAttributeBase class.