Compass.NET
6.0.1
dotnet add package Compass.NET --version 6.0.1
NuGet\Install-Package Compass.NET -Version 6.0.1
<PackageReference Include="Compass.NET" Version="6.0.1" />
paket add Compass.NET --version 6.0.1
#r "nuget: Compass.NET, 6.0.1"
// Install Compass.NET as a Cake Addin #addin nuget:?package=Compass.NET&version=6.0.1 // Install Compass.NET as a Cake Tool #tool nuget:?package=Compass.NET&version=6.0.1
Compass.NET
Simple HTTP request router for .NET backends
This documentation refers the v6.X of the library
How to use
This library comes with an extremely simple API set (consits of a few methods only)
Register the known routes
using System.Collections.Generic; using System.Net; using Solti.Utils.Router; RouterBuilder routerBuilder = new ( // This handler is called on every unknown routes handler: (object? state, HttpStatusCode reason) => { HttpListenerContext ctx = (HttpListenerContext) state; ... }, // can be omitted converters: DefaultConverters.Instance ); routerBuilder.AddRoute ( // A route may contain parameter(s) route: "/get/picture-{id:int}", handler: (IReadOnlyDictionary<string, object?> paramz, object? state) => { HttpListenerContext ctx = (HttpListenerContext) state; int id = (int) paramz["id"]; ... }, // "GET" is the default "GET", "OPTIONS" );
A valid route looks like
[/]segment1/[prefix]{paramName:converter[:style]}[suffix]/segment3[/]
Build the router delegate and start the HTTP backend
Router route = routerBuilder.Build(); HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8080/"); listener.Start(); ... while (Listener.IsListening) // probably this will run in a separate thread { HttpListenerContext context = Listener.GetContext(); route(context, context.Request.Url!.AbsolutePath.AsSpan(), context.Request.HttpMethod.AsSpan()); }
For a more comprehensive example check out the use cases fixture
Converters
Converters are used to parse variable value coming from the request path. Default converters (int
, float
, guid
, date
, str
and enum
) can be accessed via the DefaultConverters.Instance
property.
using System.Collections.Generic;
using Solti.Utils.Router;
RouterBuilder routerBuilder = new
(
defaultHandler: (object? state) => {...},
converters: new Dictionary<string, ConverterFactory>(DefaultConverters.Instance)
{
{"mytype", static (string? style) => new MyConverter(style)}
}
);
class MyConverter: IConverter
{
public string Id { get; }
public string? Style { get; }
public bool ConvertToValue(ReadOnySpan<char> input, out object? value) { ... }
public bool bool ConvertToString(object? input, out string? value) { ... }
public MyConverter(string? style)
{
Id = $"{GetType().Name}:{style}";
Style = style;
}
}
Building routes from template
using System.Collections.Generic;
using Solti.Utils.Router;
RouteTemplateCompiler compile = RouteTemplate.CreateCompiler("http://localhost:8080/get/picture-{id:int}");
string route = compile(new Dictionary<string, object?> { { "id", 1986 } }); // route == "http://localhost:8080/get/picture-1986"
...
Advanced usage
Async routing
In real world, request handlers often contain complex, async logic. AsyncRouterBuilder
is aimed to support this use case with an API set very similar to RouterBuilder
:
using System.Collections.Generic;
using System.Net;
using Solti.Utils.Router;
AsyncRouterBuilder routerBuilder = AsyncRouterBuilder.Create
(
handler: async (object? state, HttpStatusCode reason) =>
{
HttpListenerContext ctx = (HttpListenerContext) state;
await ...
},
// can be omitted
converters: DefaultConverters.Instance
);
routerBuilder.AddRoute
(
route: "/get/picture-{id:int}",
handler: async (IReadOnlyDictionary<string, object?> paramz, object? state) =>
{
HttpListenerContext ctx = (HttpListenerContext) state;
int id = (int) paramz["id"];
await ...
},
// "GET" is the default
"GET", "OPTIONS"
);
routerBuilder.AddRoute
(
route: "/",
// non-async callbacks also supported
handler: (IReadOnlyDictionary<string, object?> paramz, object? state) =>
{
...
}
);
AsyncRouter route = routerBuilder.Build();
...
HttpListenerContext context = Listener.GetContext();
object? result = await route(context, context.Request.Url!.AbsolutePath.AsSpan(), context.Request.HttpMethod.AsSpan());
IoC backed routing
Since request handlers may contain complex logic and they usually depend on another services, it's a suggested practice to grab them via dependency injection:
using System.Collections.Generic;
using System.IO;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Solti.Utils.Router;
using ResponseData = (HttpStatusCode Status, object? Body);
AsyncRouterBuilder routerBuilder = AsyncRouterBuilder.Create
(
handler: async (object? state, HttpStatusCode reason) =>
{
return new ResponseData(HttpStatusCode.NotFound, "Not found");
}
);
routerBuilder.AddRoute
(
route: "/get/picture-{id:int}",
handler: async (IReadOnlyDictionary<string, object?> paramz, object? state) =>
{
IServiceProvider svcProvider = (IServiceProvider) state;
int id = (int) paramz["id"];
return await svcProvider.GetService(typeof(PictureStore)).GetPicture(id);
}
);
...
ServiceCollection services = new();
services.AddScoped<PictureStore>();
ServiceProvider serviceProvider = services.BuildServiceProvider();
...
using(IServiceScope scope = serviceProvider.CreateScope())
{
HttpListenerContext context = Listener.GetContext();
object? response = await route(scope.ServiceProvider, context.Request.Url!.AbsolutePath, context.Request.HttpMethod);
context.Response.ContentType = "application/json";
if (response is ResponseData responseData)
{
context.Response.StatusCode = (int) responseData.Status;
response = responseData.Body;
}
else
{
context.Response.StatusCode = (int) HttpStatusCode.OK;
}
using (StreamWriter streamWriter = new(context.Response.OutputStream))
{
streamWriter.Write(JsonSerializer.Serialize(response));
}
context.Response.Close();
}
The Solti.Utils.Router.Extensions
namespace aims to simplify the route registration described above:
using Solti.Utils.Router;
using Solti.Utils.Router.Extensions;
routerBuilder.AddRoute<PictureStore>
(
"/get/picture-{id:int}",
store => store.GetPicture(default) // GetPicture() should have only one parameter named "id"
);
// OR
routerBuilder.AddRoute<PictureStore>
(
"/get/picture-{id:int}",
method // MethodInfo object pointing to GetPicture()
);
For a more comprehensive example check out the use cases fixture
Error handling
You can register your own (even async
) exception handler to be built into the router delegate
using System;
using Solti.Utils.Router;
RouterBuilder routerBuilder = new();
routerBuilder.RegisterExceptionHandler(handler: (object? state, MyException exception) => exception);
routerBuilder.AddRoute("/fail", handler: (IReadOnlyDictionary<string, object?> paramz, object? state) => throw new MyException());
Router route = routerBuilder.Build();
Assert.That(route(null, "/fail", "GET"), Is.InstanceOf<MyException>());
or
using System;
using Solti.Utils.Router;
AsyncRouterBuilder routerBuilder = AsyncRouterBuilder.Create();
routerBuilder.RegisterExceptionHandler(handler: (object? state, MyException exception) => Task.FromResult(exception));
routerBuilder.AddRoute("/fail", handler: (IReadOnlyDictionary<string, object?> paramz, object? state) => Task.FromException<TAny>(new MyException()));
AsyncRouter route = routerBuilder.Build();
Assert.That(await route(null, "/fail", "GET"), Is.InstanceOf<MyException>());
Route parsing
Lets suppose we want to validate route parameters if they meet a given condition. In this case we may utilize the RouteTemplate.Parse()
method:
using System.Reflection;
using Solti.Utils.Router;
void Validate(ParameterInfo[] expected, string route)
{
ParsedRoute parsed = RouteTemplate.Parse(route);
if (parsed.Parameters.Count != expected.Length)
throw ...;
foreach (ParameterInfo param in expected)
{
if (!parsed.Parameters.TryGetValue(param.Name, out Type t) || param.Type != t)
throw ...;
}
...
}
Resources
Supported frameworks
This project currently targets .NET Standard 2.0 and 2.1. and had been tested against net472
, netcoreapp3.1
, net5.0
, net6.0
, net7.0
and net8.0
.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. 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. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Injector.NET.Interfaces (>= 10.1.0)
- Solti.Utils.Primitives (>= 8.4.0)
- System.Memory (>= 4.5.5)
- System.Runtime.CompilerServices.Unsafe (>= 6.0.0)
-
.NETStandard 2.1
- Injector.NET.Interfaces (>= 10.1.0)
- Solti.Utils.Primitives (>= 8.4.0)
- System.Runtime.CompilerServices.Unsafe (>= 6.0.0)
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 |
---|---|---|
6.0.1 | 74 | 8/4/2024 |
6.0.0 | 71 | 8/4/2024 |
5.0.5 | 106 | 5/22/2024 |
5.0.4 | 127 | 2/23/2024 |
5.0.3 | 117 | 2/8/2024 |
5.0.2 | 98 | 2/8/2024 |
5.0.1 | 106 | 2/8/2024 |
5.0.0 | 109 | 1/31/2024 |
4.0.3 | 112 | 1/25/2024 |
4.0.2 | 101 | 1/25/2024 |
4.0.1 | 101 | 1/23/2024 |
4.0.0 | 104 | 1/23/2024 |
3.0.0 | 157 | 1/9/2024 |
3.0.0-preview1 | 115 | 1/8/2024 |
2.0.1 | 157 | 12/29/2023 |
2.0.0 | 211 | 10/16/2023 |
1.2.1 | 139 | 9/18/2023 |
1.2.0 | 135 | 9/17/2023 |
1.1.1 | 124 | 9/15/2023 |
1.1.0 | 139 | 9/15/2023 |
1.0.0 | 144 | 9/13/2023 |
1.0.0-preview5 | 126 | 9/12/2023 |
1.0.0-preview4 | 134 | 9/5/2023 |
1.0.0-preview3 | 128 | 9/3/2023 |
1.0.0-preview2 | 121 | 8/25/2023 |
1.0.0-preview1 | 120 | 8/20/2023 |