SimpleW 11.3.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package SimpleW --version 11.3.2                
NuGet\Install-Package SimpleW -Version 11.3.2                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SimpleW" Version="11.3.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add SimpleW --version 11.3.2                
#r "nuget: SimpleW, 11.3.2"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install SimpleW as a Cake Addin
#addin nuget:?package=SimpleW&version=11.3.2

// Install SimpleW as a Cake Tool
#tool nuget:?package=SimpleW&version=11.3.2                

SimpleW

NuGet

SimpleW is a Simple Web Server library in NET7 (windows/linux/macos).<br> It brings an easy layer on top of the great NetCoreServer socket server write by chronoxor in pure C#.

Summary

Features

  1. Routing
  2. Serve Static Files
  3. Serve RestAPI (Controller/Medthod + automatic json serialization/deserialization)
  4. Integrated JWT Authentication
  5. Websocket

Installation

Using SimpleW nuget package

dotnet add package SimpleW

Note : SimpleW depends Newtonsoft.Json package for json serialization/deserialization.

It will be replaced in futur by the native System.Text.Json as soon as some advanced features will be covered (Populate and streamingContextObject, see WIP).

Usage

Routes

In SimpleW, all is about routing and there are 2 differents kind of routes :

  • statics : for serving statics files (html, js, css, png...)
  • dynamics : for serving API (C# code)

Note : Reflection is only used to list routes, once, before server start. An expression tree is built to call method fast without using any T.GetMethod().Invoke().

Serve Statics Files

Basic Static Example

To serve statics files with a very few lines of code :

using System;
using System.Net;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // serve static content located in you folder "C:\www\" to "/" endpoint
            server.AddStaticContent(@"C:\www\", "/");

            // enable autoindex if not index.html exists in the directory
            server.AutoIndex = true;

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/");

            // block console for debug
            Console.ReadKey();
        }
    }
}

Then just point your browser to http://localhost:2015/.

Note : if AutoIndex is false and the directory does not contain a default document index.html, an http 404 error will return.

Note : on Windows, the Firewall can block this simple console app even if expose on localhost and port > 1024. You need to allow access else you will not reach the server.

Multiples Directories

SimpleW can handle multiples directories as soon as they are declared under differents endpoints.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // serve directories/endpoints
            server.AddStaticContent(@"C:\www\frontend", "/");
            server.AddStaticContent(@"C:\www\public", "/public/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/");

            // block console for debug
            Console.ReadKey();
        }
    }
}

Options

You can change some settings before server start.

To change the default document

server.DefaultDocument = "maintenance.html";

To add custom mimeTypes

server.AddMimeTypes(".vue", "text/html");

Cache

The AddStaticContent() cache all directories/files in RAM (default: 1 hour) on server start.<br /> Also, an internal filesystem watcher is maintaining this cache up to date. It supports realtime file editing even when specific lock/write occurs.

To modify cache duration or filter files

// serve statics files
server.AddStaticContent(
    @"C:\www\",             // under C:\www or its subdirectories
    "/",                    // to / endpoint
    "*.csv",                // only CSV files
    TimeSpan.FromDays(1)    // set cache to 1 day
);

Serve RestAPI

Basic RestAPI Example

As already said, the RestAPI is based on routes.<br /> So juste add a RouteAttribute to target methods of a Controller base class.<br /> The return is serialized into json and sent as response to the client.

Use server.AddDynamicContent() to handle RestAPI.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // find all Controllers class and serve on the "/api/" endpoint
            server.AddDynamicContent("/api/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/api/");

            // block console for debug
            Console.ReadKey();
        }
    }

    // a Controller base class
    public class SomeController : Controller {

        // use the Route attribute to target a public method
        [Route("GET", "test")]
        public object SomePublicMethod() {
            // the return will be serialized to json
            return new {
                hello = "world"
            };
        }

    }

}

Then just open your browser to http://localhost:2015/api/test and you will see the { "hello": "world" } json response.

Note : the controller must not have a constructor.

Return Type

Any return type (object, List, Dictionary, String...) will be serialized and sent as json to the client.

The following example illustrates differents return types :

using System;
using System.Net;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // find all Controllers class and serve on the "/api/" endpoint
            server.AddDynamicContent("/api/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/api/");

            // block console for debug
            Console.ReadKey();
        }
    }

    public class TestController : Controller {

        [Route("GET", "test1")]
        public object Test1() {
            // return: { "hello": "world", "date": "2023-10-23T00:00:00+02:00", "result": true }
            return new {
                hello = "world",
                date = new DateTime(2023, 10, 23),
                result = true
            };
        }

        [Route("GET", "test2")]
        public object Test2() {
            // return: ["hello", "world"]
            return new string[] { "hello", "world" };
        }

    }

    public class UserController : Controller {

        [Route("GET", "users")]
        public object Users() {
            // return: [{"Email":"user1@localhost","FullName":"user1"},{"Email":"user2@localhost","FullName":"user2"}]
            var users = new List<User>() {
                new User() { Email = "user1@localhost", FullName = "user1" },
                new User() { Email = "user2@localhost", FullName = "user2" },
            };
            return users;
        }

    }

    // example class
    public class User {
        // these public properties will be serialized
        public string Email { get; set; }
        public string FullName { get ;set; }
        // private will not be serialized
        private bool Enabled = false;
    }

}

To see the results, open your browser to :

  • http://localhost:2015/api/test1
  • http://localhost:2015/api/test2
  • http://localhost:2015/api/users

Note : there is no need to specify the exact type the method will return. Most of the time, object is enougth and will be passed to a JsonConvert.SerializeObject(object).

Return Helpers

In fact, the Controller class is dealing with an HttpResponse object which is sent async to the client.<br /> You can manipulate this object with the property Response.

There are also some useful helpers which facilitate returning specific HttpReponse :

using System;
using System.Net;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // find all Controllers class and serve on the "/api/" endpoint
            server.AddDynamicContent("/api/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/api/");

            // block console for debug
            Console.ReadKey();
        }
    }

    public class TestController : Controller {

        [Route("GET", "test1")]
        public object Test1() {
            // the object return will be serialized
            // and set as body of the HttpReponse
            // and a mimetype json
            // with a status code 200
            return new { hello = "world" };
        }

        [Route("GET", "test2")]
        public object Test2() {
            try {
                throw new Exception("test2");
            }
            catch (Exception ex) {
                // set message exception as body of the HttpReponse
                // and a mimetype text
                // with a status code 500
                return MakeInternalServerErrorResponse(ex.Message);
            }
        }

        [Route("GET", "test3")]
        public object Test3() {
            try {
                throw new KeyNotFoundException("test3");
            }
            catch (Exception ex) {
                // set message exception as body of the HttpReponse
                // and a mimetype text
                // with a status code 404
                return MakeNotFoundResponse(ex.Message);
            }
        }

        [Route("GET", "test4")]
        public object Test4() {
            try {
                throw new UnauthorizedAccessException("test4");
            }
            catch (Exception ex) {
                // set message exception as body of the HttpReponse
                // and a mimetype text
                // with a status code 401
                return MakeUnAuthorizedResponse(ex.Message);
            }
        }

        [Route("GET", "test5")]
        public object Test5() {
            var content = "download text content";
            // will force download a file "file.txt" with content
            return MakeDownloadResponse(content, "file.txt");
        }

    }

}

Note : all these helpers support differents types of parameters and options to deal with most of the use cases. Just browse to discover all the possibilities.

Edge Cases of Return

Documentation in progress...

Routing

Each route is a concatenation of :

  1. Prefix defined by AddDynamicContent().
  2. Route attribute on Controller class (if exists).
  3. Route attribute on Method.
Examples

Route attribute on methods.

using System;
using System.Net;
using SimpleW;

namespace Sample {

    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    public class TestController : Controller {
        
        // call on GET http://localhost:2015/api/test/index
        [Route("GET", "test/index")]
        public object Index() {
            return "test index page";
        }

        // call POST http://localhost:2015/api/test/create
        [Route("POST", "test/create")]
        public object Create() {
            return "test create success";
        }

    }
}

The same example can be refactored with Route attribute on controller class.

using System;
using System.Net;
using SimpleW;

namespace Sample {

    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    [Route("test/")]
    public class TestController : Controller {
        
        // call on GET http://localhost:2015/api/test/index
        [Route("GET", "index")]
        public object Index() {
            return "test index page";
        }

        // call POST http://localhost:2015/api/test/create
        [Route("POST", "create")]
        public object Create() {
            return "test create success";
        }

    }
}

You can override a method Route with the parameter isAbsolutePath: true.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    
    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    [Route("test/")]
    public class TestController : Controller {

        // test with http://localhost:2015/api/test/index
        [Route("GET", "index")]
        public object Index() {
            return "test index page";
        }

        // test with http://localhost:2015/home
        [Route("GET", "/home", isAbsolutePath: true)]
        public object Home() {
            return "home page";
        }

        // test with POST http://localhost:2015/api/test/create
        [Route("POST", "create")]
        public object Create() {
            return "test create success";
        }

        // test with POST http://localhost:2015/api/test/delete
        // or
        // test with POST http://localhost:2015/api/test/remove
        [Route("POST", "delete")]
        [Route("POST", "remove")]
        public object Delete() {
            return "test delete success";
        }

    }
}

Note :

  • the isAbsolutePath flag will not take the prefix defined in the AddDynamicContent.
  • methods can have multiple Route Attributes (example above with delete, remove).
Regexp

Route path support regulars expressions.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    
    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    [Route("test/")]
    public class TestController : Controller {

        // http://localhost:2015/api/test/index
        // or
        // http://localhost:2015/api/test/indexes
        [Route("GET", "(index|indexes)")]
        public object Index() {
            return "test index page";
        }

    }
}
QueryString Parameters

Query String parameters are also supported in a similar way. The library will map query string parameter to the method parameter.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    
    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    public class TestController : Controller {

        // test with http://localhost:2015/api/hello
        // test with http://localhost:2015/api/hello?name=stratdev
        //
        // parameter "name" has default value "world" 
        // so the query string "name" is not mandatory
        [Route("GET", "hello")]
        public object Hello(string name = "world") {
            return $"Hello {name} !";
        }

        // test with http://localhost:2015/api/hi?name=stratdev
        // test with http://localhost:2015/api/hi
        //
        // parameter "name" has no default value
        // so the query string "name" is required
        // not providing it will return an HTTP 404 ERROR
        [Route("GET", "hi")]
        public object Hi(string name) {
            return $"Hi {name} !";
        }

        // test with http://localhost:2015/api/bye?name=stratdev&exit=0
        //
        // it does not matter if there are others query strings 
        // than the one declared in the method
        [Route("GET", "bye")]
        public object Bye(string name) {
            return $"Bye {name} !";
        }

        // test with http://localhost:2015/api/debug?a=bbbb&c=dddd
        [Route("GET", "debug")]
        public object Debug() {
            try {
                // get NameValueCollection
                var nvc = Route.ParseQueryString(this.Request.Url);
                // convert to dictionnary
                var querystrings = nvc.AllKeys.ToDictionary(k => k, k => nvc[k]);
                return new {
                    message = $"list query string parameters from {this.Request.Url}",
                    querystrings
                };
            }
            catch (Exception ex) {
                return MakeInternalServerErrorResponse(ex.Message);
            }
        }

    }
}

Notes :

  • QueryString are map by name to the parameter method.
  • Only declared parameters are map.
  • When a method has a mandatory parameter (without default value), the route will not match if not provided in the url (return HTTP CODE 404).
Path Parameters

Route path parameters are also supported in a similar way. When a {parameter} is declared in the path, it's possible to set parameter in Route path and retrieve their value in the method.<br /> The library will map them according to their name.

using System;
using System.Net;
using SimpleW;

namespace Sample {
    
    class Program {
        static void Main() {
            var server = new SimpleWServer(IPAddress.Any, 2015);
            server.AddDynamicContent("/api/");
            server.Start();
            Console.ReadKey();
        }
    }

    [Route("test/")]
    public class TestController : Controller {

        // test with http://localhost:2015/api/test/user/stratdev
        [Route("GET", "user/{login}")]
        public object User(string login) {
            return $"Hello {login}";
        }

        // test with http://localhost:2015/api/test/stratdev/2023
        // but
        // test with http://localhost:2015/api/test/stratdev/xx will
        // return a http code 500 as the "xx" cast to integer
        // will thrown an exception
        [Route("GET", "user/{login}/{year}")]
        public object User(string login, int year) {
            return $"Hello {login}, you're {year} year old.";
        }

    }
}

Note :

  • In this example, the value stratdev of {login} parameter will be map to string login and the value 2023 of {year} parameter will be map to int year.
  • the string value of parameter will be cast to the parameter type. If the cast failed, an HTTP CODE 500 will be return to the client.
  • all declared parameters in Route path are mandatory.

POST body (application/json) deserialization

You can use the BodyMap() method for reading POST body and deserialize to an object instance.

Frontend send POST json data

var json_payload = {
    id: "c037a13c-5e77-11ec-b466-e33ffd960c3a",
    name: "test",
    creation: "2021-12-21T15:06:58",
    enabled: true
};
axios.post("http://localhost:2015/api/user/save", json_payload);

Backend receive

public class User {
    public Guid id;
    public string name;
    public DateTime creation;
    public bool enabled;
}

[Route("POST", "user/save")]
public object Save() {

    var user = new User();

    // map POST body JSON to object instance
    Request.BodyMap(user);

    return new {
        user
    };
}

Note :

  • the content-type set by client need to be application/json which is the default for axios.

POST body (application/x-www-form-urlencoded) deserialization

You can use the BodyMap() method for reading POST body and deserialize to an object instance.

Frontend send POST json data

const form_payload = {
    id: "c037a13c-5e77-11ec-b466-e33ffd960c3a",
    name: "test",
    creation: "2021-12-21T15:06:58",
    enabled: true
};

const options = {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  // encode payload to format "key=value&key=value.."
  data: Object.keys(form_payload)
              .map((key) => `${key}=${encodeURIComponent(form_payload[key])}`)
              .join('&')
  url: "http://localhost:2015/api/user/save",
};
axios(options);

Backend receive

public class User {
    public Guid id;
    public string name;
    public DateTime creation;
    public bool enabled;
}

[Route("POST", "user/save")]
public object Save() {

    var user = new User();

    // map POST body FORM to object instance
    Request.BodyMap(user);

    return new {
        user
    };
}

Serialization

Default

The public object method will be serialized to json using the excellent JsonConvert.SerializeObject() from Newtonsoft.Json

[Route("GET", "test")]
public object Test() {
    return new {
        hello = "Hello World !",
        current = Datetime.Now,
        i = 0,
        d = new Dictionary<string, string>() { { "Foo", "Bar" } }
    };
}

Requesting to http://localhost:2015/api/test will result to

{
    "hello": "Hello World !",
    "current": "2021-12-15T11:44:29.1249399+01:00",
    "i": 0,
    "d": {"Foo":"Bar"}
}
Request

You can access the Request property inside any controller.

Responses

You can access the Response property inside any controller.

3. JWT Authentication

Documentation in progress...

4. Websockets

Documentation in progress...

OpenTelemetry

SimpleW handle an opentelemetry Activity and publish Event. As such, you can subscribe to this source and ...

See an example which log all request to console (do not use for production). Open browser to http://localhost:2015/api/test and console will show log.

using System;
using System.Net;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using OpenTelemetry;
using OpenTelemetry.Trace;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // subscribe to all SimpleW events
            openTelemetryObserver("SimpleW");

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // find all Controllers class and serve on the "/api/" endpoint
            server.AddDynamicContent("/api/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/api/");

            // block console for debug
            Console.ReadKey();
        }

        static TracerProvider openTelemetryObserver(string source) {
            return Sdk.CreateTracerProviderBuilder()
                              .AddSource(source)
                              .AddProcessor(new LogProcessor()) // custom log processor
                              .SetResourceBuilder(
                                  ResourceBuilder
                                      .CreateEmpty()
                                      .AddService(serviceName: "Sample", serviceVersion: "0.1")
                              ).Build();
        }

    }

    // custom log processor
    class LogProcessor : BaseProcessor<Activity> {
        // write log to console
        public override void OnEnd(Activity activity) {
              Console.WriteLine($"{activity.GetTagItem("http.request.method")} \"{activity.GetTagItem("url.full")}\" {activity.GetTagItem("http.response.status_code")} {(int)activity.Duration.TotalMilliseconds}ms session-{activity.GetTagItem("session")} {activity.GetTagItem("client.address")} \"{activity.GetTagItem("user_agent.original")}\"");
        }
    }

    public class SomeController : Controller {
        [Route("GET", "test")]
        public object SomePublicMethod() {
            return new {
                hello = "world"
            };
        }
    }

}

For production grade, better to use well known solutions.

Uptrace is one of them can be easily integrate thanks to the Uptrace nuget package

See example

using System;
using System.Net;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using OpenTelemetry;
using OpenTelemetry.Trace;
using Uptrace.OpenTelemetry;
using SimpleW;

namespace Sample {
    class Program {

        static void Main() {

            // subscribe to all SimpleW events
            openTelemetryObserver("SimpleW");

            // listen to all IPs port 2015
            var server = new SimpleWServer(IPAddress.Any, 2015);

            // find all Controllers class and serve on the "/api/" endpoint
            server.AddDynamicContent("/api/");

            server.Start();

            Console.WriteLine("server started at http://localhost:2015/");

            // block console for debug
            Console.ReadKey();
        }

        static TracerProvider openTelemetryObserver(string source) {
            return Sdk.CreateTracerProviderBuilder()
                              .AddSource(source)
                              // see https://uptrace.dev/get/get-started.html#dsn
                              .AddUptrace(uptrace_connection_string)
                              .SetResourceBuilder(
                                  ResourceBuilder
                                      .CreateEmpty()
                                      .AddService(serviceName: "Sample", serviceVersion: "0.1")
                              ).Build();
        }

    }

    public class SomeController : Controller {
        [Route("GET", "test")]
        public object SomePublicMethod() {
            return new {
                hello = "world"
            };
        }
    }

}

License

This library is under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
12.0.0 608 5/1/2024
11.9.0 591 2/15/2024
11.8.0 133 2/14/2024
11.7.0 96 2/3/2024
11.6.0 103 2/2/2024
11.5.0 349 1/14/2024
11.4.0 126 1/4/2024
11.3.3 160 12/24/2023
11.3.2 124 12/24/2023
11.3.1 121 12/24/2023
11.3.0 198 12/15/2023
11.2.0 127 12/9/2023
11.1.0 130 11/7/2023
11.0.0 124 11/1/2023
10.0.0 340 10/22/2023
9.0.1 138 10/22/2023
9.0.0 122 10/21/2023
8.2.0 181 10/18/2023
8.1.2 164 10/12/2023
8.1.1 122 10/12/2023
8.1.0 116 10/12/2023
8.0.8 390 8/17/2023
8.0.7 139 8/16/2023
8.0.6 584 5/15/2023
8.0.5 137 5/15/2023
8.0.4 462 3/30/2023
8.0.3 227 3/29/2023
8.0.2 323 3/19/2023
8.0.1 410 2/16/2023
8.0.0 372 1/25/2023
7.0.0 555 12/1/2022
6.2.4 333 11/30/2022
6.2.3 324 11/30/2022
6.2.2 438 11/10/2022
6.2.1 465 10/20/2022
6.2.0 442 10/5/2022
6.1.0 373 10/4/2022
6.0.0 383 10/2/2022
5.0.0 399 9/30/2022
4.4.6 419 9/28/2022
4.4.5 405 9/28/2022
4.4.4 386 9/27/2022
4.4.3 552 8/29/2022
4.4.2 410 8/24/2022
4.4.1 436 8/24/2022
4.4.0 411 8/24/2022
4.3.2 523 8/13/2022
4.3.1 395 8/13/2022
4.3.0 420 8/11/2022
4.2.8 507 7/29/2022
4.2.7 619 6/28/2022
4.2.6 439 6/28/2022
4.2.5 446 6/14/2022
4.2.4 653 4/27/2022
4.2.3 403 4/27/2022
4.2.2 590 4/7/2022
4.2.1 430 4/7/2022
4.2.0 453 4/4/2022
4.1.0 496 3/22/2022
4.0.0 408 3/21/2022
3.2.0 428 3/20/2022
3.1.2 509 3/10/2022
3.1.1 448 3/8/2022
3.1.0 474 2/22/2022
3.0.0 455 2/21/2022
2.3.0 491 2/11/2022
2.2.1 473 2/3/2022
2.1.1 452 1/31/2022
2.1.0 426 1/30/2022
2.0.0 478 1/23/2022
1.1.1 402 1/3/2022
1.1.0 305 12/26/2021
1.0.15 296 12/16/2021
1.0.14 293 12/16/2021
1.0.13 2,409 11/26/2021
1.0.12 3,888 11/24/2021
1.0.11 3,748 11/24/2021
1.0.10 5,481 11/24/2021
1.0.9 6,557 11/23/2021
1.0.8 962 11/20/2021
1.0.7 320 11/15/2021
1.0.6 297 11/15/2021
1.0.5 348 11/12/2021
1.0.4 328 11/12/2021
1.0.3 374 11/12/2021
1.0.2 362 11/11/2021
1.0.1 344 11/10/2021
1.0.0 405 11/10/2021 1.0.0 is deprecated because it has critical bugs.