Skip to content

Commit bdc7b4b

Browse files
authored
Merge pull request #617 from Kaliumhexacyanoferrat/genhttp-kestrel
[C#] GenHTTP: Add Kestrel based tests (including H2/H3)
2 parents 558cd75 + 3338384 commit bdc7b4b

64 files changed

Lines changed: 932 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
2+
WORKDIR /source
3+
4+
COPY genhttp.csproj ./
5+
RUN dotnet restore
6+
7+
COPY . .
8+
RUN dotnet publish -c Release --no-self-contained -o /app
9+
10+
FROM mcr.microsoft.com/dotnet/aspnet:10.0
11+
12+
ADD https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb /packages-microsoft-prod.deb
13+
14+
RUN dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb \
15+
&& apt-get update \
16+
&& apt-get install -y --no-install-recommends libmsquic \
17+
&& apt-get clean \
18+
&& rm -rf /var/lib/apt/lists/*
19+
20+
WORKDIR /app
21+
COPY --from=build /app .
22+
23+
EXPOSE 8080 8081 8082 8443/tcp 8443/udp
24+
ENTRYPOINT ["dotnet", "genhttp.dll"]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace genhttp;
2+
3+
public class DatasetItem
4+
{
5+
public int Id { get; set; }
6+
public string Name { get; set; } = "";
7+
public string Category { get; set; } = "";
8+
public int Price { get; set; }
9+
public int Quantity { get; set; }
10+
public bool Active { get; set; }
11+
public List<string>? Tags { get; set; }
12+
public RatingInfo? Rating { get; set; }
13+
}
14+
15+
public class ProcessedItem
16+
{
17+
public int Id { get; set; }
18+
public string Name { get; set; } = "";
19+
public string Category { get; set; } = "";
20+
public int Price { get; set; }
21+
public int Quantity { get; set; }
22+
public bool Active { get; set; }
23+
public List<string>? Tags { get; set; }
24+
public RatingInfo? Rating { get; set; }
25+
public long Total { get; set; }
26+
}
27+
28+
public class RatingInfo
29+
{
30+
public int Score { get; set; }
31+
public int Count { get; set; }
32+
}
33+
34+
public class ListWithCount<T>(List<T> items)
35+
{
36+
37+
public List<T> Items => items;
38+
39+
public int Count => items.Count;
40+
41+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Net;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
using genhttp;
5+
6+
using GenHTTP.Engine.Kestrel;
7+
using GenHTTP.Modules.Compression;
8+
9+
var certPath = Environment.GetEnvironmentVariable("TLS_CERT") ?? "/certs/server.crt";
10+
var keyPath = Environment.GetEnvironmentVariable("TLS_KEY") ?? "/certs/server.key";
11+
var hasCert = File.Exists(certPath) && File.Exists(keyPath);
12+
13+
var app = Project.Create();
14+
15+
var host = Host.Create()
16+
.Handler(app)
17+
.Compression();
18+
19+
host.Bind(IPAddress.Any, 8080);
20+
21+
if (hasCert)
22+
{
23+
host.Bind(IPAddress.Any, 8081, X509Certificate2.CreateFromPemFile(certPath, keyPath));
24+
host.Bind(IPAddress.Any, 8443, X509Certificate2.CreateFromPemFile(certPath, keyPath), enableQuic: true);
25+
}
26+
27+
await host.RunAsync();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using GenHTTP.Api.Content;
2+
using GenHTTP.Modules.IO;
3+
using GenHTTP.Modules.Layouting;
4+
using GenHTTP.Modules.Layouting.Provider;
5+
using GenHTTP.Modules.Webservices;
6+
using GenHTTP.Modules.Websockets;
7+
8+
using genhttp.Tests;
9+
10+
namespace genhttp;
11+
12+
public static class Project
13+
{
14+
public static IHandlerBuilder Create()
15+
{
16+
var app = Layout.Create()
17+
.Add("pipeline", Content.From(Resource.FromString("ok")))
18+
.AddService<Baseline>("baseline11")
19+
.AddService<Baseline>("baseline2")
20+
.AddService<Upload>("upload")
21+
.AddService<Json>("json")
22+
.AddService<AsyncDatabase>("async-db")
23+
.AddStaticFiles()
24+
.AddWebsocket();
25+
26+
return app;
27+
}
28+
29+
private static LayoutBuilder AddStaticFiles(this LayoutBuilder app)
30+
{
31+
if (Directory.Exists("/data/static"))
32+
{
33+
app.Add("static", Resources.From(ResourceTree.FromDirectory("/data/static")));
34+
}
35+
36+
return app;
37+
}
38+
39+
private static LayoutBuilder AddWebsocket(this LayoutBuilder app)
40+
{
41+
var websocket = Websocket.Imperative()
42+
.DoNotAllocateFrameData()
43+
.Handler(new EchoHandler());
44+
45+
return app.Add("ws", websocket);
46+
}
47+
48+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# GenHTTP
2+
3+
Lightweight embeddable C# web server using the GenHTTP library on the Kestrel engine.
4+
5+
## Stack
6+
7+
- **Language:** C# / .NET 10 (Alpine)
8+
- **Framework:** GenHTTP
9+
- **Engine:** GenHTTP
10+
11+
## Endpoints
12+
13+
| Endpoint | Method | Description |
14+
|----------|--------|-------------|
15+
| `/pipeline` | GET | Returns `ok` (plain text) |
16+
| `/baseline11` | GET | Sums query parameter values |
17+
| `/baseline11` | POST | Sums query parameters + request body |
18+
| `/baseline2` | GET | Sums query parameter values (HTTP/2 variant) |
19+
| `/json` | GET | Processes 50-item dataset, serializes JSON |
20+
| `/compression` | GET | Gzip-compressed large JSON response |
21+
| `/db` | GET | SQLite range query with JSON response |
22+
| `/upload` | POST | Receives 1 MB body, returns byte count |
23+
| `/static/{filename}` | GET | Serves preloaded static files with MIME types |
24+
25+
## Notes
26+
27+
- Implemented via web services and a layout router
28+
- Compression and routing modules
29+
- Self-contained single-file deployment
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System.Text.Json;
2+
3+
using GenHTTP.Modules.Webservices;
4+
5+
using Npgsql;
6+
7+
namespace genhttp.Tests;
8+
9+
public class AsyncDatabase
10+
{
11+
private static readonly NpgsqlDataSource? PgDataSource = OpenPgPool();
12+
13+
private static NpgsqlDataSource? OpenPgPool()
14+
{
15+
var dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
16+
if (string.IsNullOrEmpty(dbUrl)) return null;
17+
try
18+
{
19+
var uri = new Uri(dbUrl);
20+
var userInfo = uri.UserInfo.Split(':');
21+
var connStr = $"Host={uri.Host};Port={uri.Port};Username={userInfo[0]};Password={userInfo[1]};Database={uri.AbsolutePath.TrimStart('/')};Maximum Pool Size=256;Minimum Pool Size=64;Multiplexing=true;No Reset On Close=true;Max Auto Prepare=4;Auto Prepare Min Usages=1";
22+
var builder = new NpgsqlDataSourceBuilder(connStr);
23+
return builder.Build();
24+
}
25+
catch { return null; }
26+
}
27+
28+
[ResourceMethod]
29+
public async Task<ListWithCount<object>> Compute(int min = 10, int max = 50, int limit = 50)
30+
{
31+
if (PgDataSource == null)
32+
{
33+
return new ListWithCount<object>(new List<object>());
34+
}
35+
36+
await using var cmd = PgDataSource.CreateCommand(
37+
"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3");
38+
39+
cmd.Parameters.AddWithValue(min);
40+
cmd.Parameters.AddWithValue(max);
41+
cmd.Parameters.AddWithValue(limit);
42+
43+
await using var reader = await cmd.ExecuteReaderAsync();
44+
45+
var items = new List<object>(limit);
46+
47+
while (await reader.ReadAsync())
48+
{
49+
items.Add(new
50+
{
51+
id = reader.GetInt32(0),
52+
name = reader.GetString(1),
53+
category = reader.GetString(2),
54+
price = reader.GetInt32(3),
55+
quantity = reader.GetInt32(4),
56+
active = reader.GetBoolean(5),
57+
tags = JsonSerializer.Deserialize<List<string>>(reader.GetString(6)),
58+
rating = new { score = reader.GetInt32(7), count = reader.GetInt32(8) },
59+
});
60+
}
61+
62+
return new ListWithCount<object>(items);
63+
}
64+
65+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using GenHTTP.Api.Protocol;
2+
using GenHTTP.Modules.Reflection;
3+
using GenHTTP.Modules.Webservices;
4+
5+
namespace genhttp.Tests;
6+
7+
public class Baseline
8+
{
9+
10+
[ResourceMethod]
11+
public int Sum(int a, int b) => a + b;
12+
13+
[ResourceMethod(RequestMethod.Post)]
14+
public int Sum(int a, int b, [FromBody] int c) => a + b + c;
15+
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using GenHTTP.Modules.Websockets;
2+
using GenHTTP.Modules.Websockets.Protocol;
3+
4+
namespace genhttp.Tests;
5+
6+
class EchoHandler : IImperativeHandler
7+
{
8+
public async ValueTask HandleAsync(IImperativeConnection connection)
9+
{
10+
while (true)
11+
{
12+
var frame = await connection.ReadFrameAsync();
13+
14+
if (frame.Type == FrameType.Close)
15+
break;
16+
17+
if (frame.Type == FrameType.Text || frame.Type == FrameType.Binary)
18+
{
19+
await connection.WriteAsync(frame.Data, frame.Type);
20+
}
21+
}
22+
}
23+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Text.Json;
2+
3+
using GenHTTP.Api.Content;
4+
using GenHTTP.Api.Protocol;
5+
using GenHTTP.Modules.Webservices;
6+
7+
namespace genhttp.Tests;
8+
9+
public class Json
10+
{
11+
private static readonly List<DatasetItem>? DatasetItems = LoadItems();
12+
13+
private static List<DatasetItem>? LoadItems()
14+
{
15+
var jsonOptions = new JsonSerializerOptions
16+
{
17+
PropertyNameCaseInsensitive = true,
18+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
19+
};
20+
21+
var datasetPath = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json";
22+
23+
if (File.Exists(datasetPath))
24+
{
25+
return JsonSerializer.Deserialize<List<DatasetItem>>(File.ReadAllText(datasetPath), jsonOptions);
26+
}
27+
28+
return null;
29+
}
30+
31+
[ResourceMethod(":count")]
32+
public ListWithCount<ProcessedItem> Compute(int count, int m = 1)
33+
{
34+
if (DatasetItems == null)
35+
{
36+
throw new ProviderException(ResponseStatus.InternalServerError, "No dataset");
37+
}
38+
39+
if (count > DatasetItems.Count) count = DatasetItems.Count;
40+
if (count < 0) count = 0;
41+
42+
var processed = new List<ProcessedItem>(count);
43+
44+
for (var i = 0; i < count; i++)
45+
{
46+
var d = DatasetItems[i];
47+
48+
processed.Add(new ProcessedItem
49+
{
50+
Id = d.Id, Name = d.Name, Category = d.Category,
51+
Price = d.Price, Quantity = d.Quantity, Active = d.Active,
52+
Tags = d.Tags, Rating = d.Rating,
53+
Total = d.Price * d.Quantity * m
54+
});
55+
}
56+
57+
return new(processed);
58+
}
59+
60+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using GenHTTP.Api.Protocol;
2+
using GenHTTP.Modules.Webservices;
3+
4+
namespace genhttp.Tests;
5+
6+
public class Upload
7+
{
8+
9+
[ResourceMethod(RequestMethod.Post)]
10+
public ValueTask<long> Compute(Stream input)
11+
{
12+
if (input.CanSeek)
13+
{
14+
// internal engine
15+
return ValueTask.FromResult(input.Length);
16+
}
17+
18+
// kestrel
19+
return ComputeManually(input);
20+
}
21+
22+
private async ValueTask<long> ComputeManually(Stream input)
23+
{
24+
var buffer = new byte[8192];
25+
26+
long total = 0;
27+
28+
var read = 0;
29+
30+
while ((read = await input.ReadAsync(buffer)) > 0)
31+
{
32+
total += read;
33+
}
34+
35+
return total;
36+
}
37+
38+
}

0 commit comments

Comments
 (0)