diff --git a/frameworks/genhttp/Dockerfile b/frameworks/genhttp/Dockerfile index 5e28f8bf..3f5166de 100644 --- a/frameworks/genhttp/Dockerfile +++ b/frameworks/genhttp/Dockerfile @@ -1,13 +1,18 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0-preview-alpine AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build WORKDIR /source -COPY genhttp.csproj . + +COPY genhttp.csproj ./ RUN dotnet restore -r linux-musl-x64 -COPY Program.cs . + +COPY . . + RUN dotnet publish -c Release -r linux-musl-x64 --self-contained -o /app -FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-preview-alpine +FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine WORKDIR /app COPY --from=build /app . + RUN apk add --no-cache libmsquic + EXPOSE 8080 8443/tcp 8443/udp -ENTRYPOINT ["./genhttp"] +ENTRYPOINT ["./genhttp"] \ No newline at end of file diff --git a/frameworks/genhttp/Model.cs b/frameworks/genhttp/Model.cs new file mode 100644 index 00000000..8fc1f085 --- /dev/null +++ b/frameworks/genhttp/Model.cs @@ -0,0 +1,41 @@ +namespace genhttp; + +public class DatasetItem +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Category { get; set; } = ""; + public double Price { get; set; } + public int Quantity { get; set; } + public bool Active { get; set; } + public List Tags { get; set; } = new(); + public RatingInfo Rating { get; set; } = new(); +} + +public class ProcessedItem +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Category { get; set; } = ""; + public double Price { get; set; } + public int Quantity { get; set; } + public bool Active { get; set; } + public List Tags { get; set; } = new(); + public RatingInfo Rating { get; set; } = new(); + public double Total { get; set; } +} + +public class RatingInfo +{ + public double Score { get; set; } + public int Count { get; set; } +} + +public class ListWithCount(List items) +{ + + public List Items => items; + + public int Count => items.Count; + +} \ No newline at end of file diff --git a/frameworks/genhttp/Program.cs b/frameworks/genhttp/Program.cs index 0434cb8b..9ba1d467 100644 --- a/frameworks/genhttp/Program.cs +++ b/frameworks/genhttp/Program.cs @@ -1,288 +1,13 @@ using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Text.Json; -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; -using GenHTTP.Engine.Kestrel; -using GenHTTP.Modules.Compression; -using GenHTTP.Modules.Functional; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using Microsoft.Data.Sqlite; -// JSON options -var jsonOptions = new JsonSerializerOptions -{ - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase -}; +using genhttp; -// Load small dataset — keep raw items for per-request processing -var datasetPath = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json"; -List? datasetItems = null; -if (File.Exists(datasetPath)) -{ - datasetItems = JsonSerializer.Deserialize>(File.ReadAllText(datasetPath), jsonOptions); -} +using GenHTTP.Engine.Internal; -// Load large dataset for compression — pre-serialize to bytes -byte[]? largeJsonBytes = null; -var largePath = "/data/dataset-large.json"; -if (File.Exists(largePath)) -{ - var largeItems = JsonSerializer.Deserialize>(File.ReadAllText(largePath), jsonOptions); - if (largeItems != null) - { - var processed = largeItems.Select(d => new ProcessedItem - { - Id = d.Id, Name = d.Name, Category = d.Category, - Price = d.Price, Quantity = d.Quantity, Active = d.Active, - Tags = d.Tags, Rating = d.Rating, - Total = Math.Round(d.Price * d.Quantity, 2) - }).ToList(); - largeJsonBytes = JsonSerializer.SerializeToUtf8Bytes(new { items = processed, count = processed.Count }, jsonOptions); - } -} +var app = Project.Create(); -// Pre-load static files -var staticFileMap = new Dictionary(); -var staticDir = "/data/static"; -if (Directory.Exists(staticDir)) -{ - var mimeTypes = new Dictionary - { - {".css", "text/css"}, {".js", "application/javascript"}, {".html", "text/html"}, - {".woff2", "font/woff2"}, {".svg", "image/svg+xml"}, {".webp", "image/webp"}, {".json", "application/json"} - }; - foreach (var file in Directory.GetFiles(staticDir)) - { - var name = Path.GetFileName(file); - var ext = Path.GetExtension(file); - var ct = mimeTypes.GetValueOrDefault(ext, "application/octet-stream"); - staticFileMap[name] = (File.ReadAllBytes(file), ct); - } -} - -// Open SQLite database -SqliteConnection? dbConn = null; -var dbPath = "/data/benchmark.db"; -if (File.Exists(dbPath)) -{ - dbConn = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); - dbConn.Open(); - using var pragma = dbConn.CreateCommand(); - pragma.CommandText = "PRAGMA mmap_size=268435456"; - pragma.ExecuteNonQuery(); -} - -// Helper: sum query parameters -static int SumQuery(IRequest request) -{ - int sum = 0; - foreach (var (_, value) in request.Query) - { - if (int.TryParse(value, out int n)) - sum += n; - } - return sum; -} - -// Helper: build a response from a byte array -static IResponse ByteResponse(IRequest request, byte[] data, string contentType) -{ - return request.Respond() - .Content(new MemoryStream(data)) - .Type(new FlexibleContentType(contentType)) - .Length((ulong)data.Length) - .Header("Server", "genhttp") - .Build(); -} - -// Build the handler tree -var api = Inline.Create() - .Get("/pipeline", (IRequest request) => - { - return request.Respond() - .Content(Resource.FromString("ok").Build()) - .Type(new FlexibleContentType("text/plain")) - .Header("Server", "genhttp") - .Build(); - }) - .Get("/baseline11", (IRequest request) => - { - int sum = SumQuery(request); - return request.Respond() - .Content(Resource.FromString(sum.ToString()).Build()) - .Type(new FlexibleContentType("text/plain")) - .Header("Server", "genhttp") - .Build(); - }) - .Post("/baseline11", async (IRequest request) => - { - int sum = SumQuery(request); - if (request.Content != null) - { - using var reader = new StreamReader(request.Content); - var body = await reader.ReadToEndAsync(); - if (int.TryParse(body.Trim(), out int b)) - sum += b; - } - return request.Respond() - .Content(Resource.FromString(sum.ToString()).Build()) - .Type(new FlexibleContentType("text/plain")) - .Header("Server", "genhttp") - .Build(); - }) - .Get("/baseline2", (IRequest request) => - { - int sum = SumQuery(request); - return request.Respond() - .Content(Resource.FromString(sum.ToString()).Build()) - .Type(new FlexibleContentType("text/plain")) - .Header("Server", "genhttp") - .Build(); - }) - .Get("/json", (IRequest request) => - { - if (datasetItems == null) - return request.Respond().Status(500, "No dataset").Build(); - var processed = new List(datasetItems.Count); - foreach (var d in datasetItems) - { - processed.Add(new ProcessedItem - { - Id = d.Id, Name = d.Name, Category = d.Category, - Price = d.Price, Quantity = d.Quantity, Active = d.Active, - Tags = d.Tags, Rating = d.Rating, - Total = Math.Round(d.Price * d.Quantity, 2) - }); - } - var json = JsonSerializer.Serialize(new { items = processed, count = processed.Count }, jsonOptions); - return request.Respond() - .Content(Resource.FromString(json).Build()) - .Type(new FlexibleContentType("application/json")) - .Header("Server", "genhttp") - .Build(); - }) - .Get("/compression", (IRequest request) => - { - if (largeJsonBytes == null) - return request.Respond().Status(500, "No dataset").Build(); - return ByteResponse(request, largeJsonBytes, "application/json"); - }) - .Post("/upload", async (IRequest request) => - { - using var ms = new MemoryStream(); - if (request.Content != null) - await request.Content.CopyToAsync(ms); - return request.Respond() - .Content(Resource.FromString(ms.Length.ToString()).Build()) - .Type(new FlexibleContentType("text/plain")) - .Header("Server", "genhttp") - .Build(); - }) - .Get("/db", (IRequest request) => - { - if (dbConn == null) - return request.Respond().Status(500, "DB not available").Build(); - - double min = 10, max = 50; - if (request.Query.TryGetValue("min", out var minStr) && double.TryParse(minStr, out double pmin)) - min = pmin; - if (request.Query.TryGetValue("max", out var maxStr) && double.TryParse(maxStr, out double pmax)) - max = pmax; - - using var cmd = dbConn.CreateCommand(); - cmd.CommandText = "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN @min AND @max LIMIT 50"; - cmd.Parameters.AddWithValue("@min", min); - cmd.Parameters.AddWithValue("@max", max); - using var reader = cmd.ExecuteReader(); - var items = new List(); - while (reader.Read()) - { - items.Add(new - { - id = reader.GetInt32(0), - name = reader.GetString(1), - category = reader.GetString(2), - price = reader.GetDouble(3), - quantity = reader.GetInt32(4), - active = reader.GetInt32(5) == 1, - tags = JsonSerializer.Deserialize>(reader.GetString(6)), - rating = new { score = reader.GetDouble(7), count = reader.GetInt32(8) }, - }); - } - var json = JsonSerializer.Serialize(new { items, count = items.Count }, jsonOptions); - return request.Respond() - .Content(Resource.FromString(json).Build()) - .Type(new FlexibleContentType("application/json")) - .Header("Server", "genhttp") - .Build(); - }); - -// Static file handler — register each file as a sub-route in a layout -var staticLayout = Layout.Create(); -foreach (var (name, (data, contentType)) in staticFileMap) -{ - var fileData = data; - var fileContentType = contentType; - staticLayout.Add(name, Inline.Create() - .Get((IRequest request) => ByteResponse(request, fileData, fileContentType))); -} - -var layout = Layout.Create() - .Add(api) - .Add("static", staticLayout); - -// TLS configuration -var certPath = Environment.GetEnvironmentVariable("TLS_CERT") ?? "/certs/server.crt"; -var keyPath = Environment.GetEnvironmentVariable("TLS_KEY") ?? "/certs/server.key"; -var hasCert = File.Exists(certPath) && File.Exists(keyPath); - -var host = Host.Create() - .Handler(layout) - .Compression(CompressedContent.Default()); +var host = Host.Create().Handler(app); host.Bind(IPAddress.Any, 8080); -if (hasCert) -{ - var cert = X509Certificate2.CreateFromPemFile(certPath, keyPath); - host.Bind(IPAddress.Any, 8443, cert, enableQuic: true); -} - -await host.RunAsync(); - -// --- Data models --- - -class DatasetItem -{ - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Category { get; set; } = ""; - public double Price { get; set; } - public int Quantity { get; set; } - public bool Active { get; set; } - public List Tags { get; set; } = new(); - public RatingInfo Rating { get; set; } = new(); -} - -class ProcessedItem -{ - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Category { get; set; } = ""; - public double Price { get; set; } - public int Quantity { get; set; } - public bool Active { get; set; } - public List Tags { get; set; } = new(); - public RatingInfo Rating { get; set; } = new(); - public double Total { get; set; } -} - -class RatingInfo -{ - public double Score { get; set; } - public int Count { get; set; } -} - +await host.RunAsync(); \ No newline at end of file diff --git a/frameworks/genhttp/Project.cs b/frameworks/genhttp/Project.cs new file mode 100644 index 00000000..e712bce8 --- /dev/null +++ b/frameworks/genhttp/Project.cs @@ -0,0 +1,92 @@ +using System.IO.Compression; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Compression; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +using genhttp.Tests; + +namespace genhttp; + +public static class Project +{ + public static IHandlerBuilder Create() + { + var app = Layout.Create() + .AddPipeline() + .AddBaseline() + .AddUpload() + .AddJson() + .AddDatabase() + .AddCompression() + .AddStaticFiles() + .Add(Concern.From(AddHeader)); + + return app; + } + + private static LayoutBuilder AddStaticFiles(this LayoutBuilder app) + { + var staticDir = "/data/static"; + + if (Directory.Exists(staticDir)) + { + var files = ResourceTree.FromDirectory("/data/static"); + + app.Add("static", Resources.From(files)); + } + + return app; + } + + private static LayoutBuilder AddPipeline(this LayoutBuilder app) + { + return app.Add("pipeline", Content.From(Resource.FromString("ok"))); + } + + private static LayoutBuilder AddBaseline(this LayoutBuilder app) + { + return app.AddService("baseline11", mode: ExecutionMode.Auto) + .AddService("baseline2", mode: ExecutionMode.Auto); + } + + private static LayoutBuilder AddUpload(this LayoutBuilder app) + { + return app.AddService("upload", mode: ExecutionMode.Auto); + } + + private static LayoutBuilder AddJson(this LayoutBuilder app) + { + return app.AddService("json", mode: ExecutionMode.Auto); + } + + private static LayoutBuilder AddDatabase(this LayoutBuilder app) + { + return app.AddService("db", mode: ExecutionMode.Auto); + } + + private static LayoutBuilder AddCompression(this LayoutBuilder app) + { + var service = ServiceResource.From().ExecutionMode(ExecutionMode.Auto); + + service.Add(CompressedContent.Default().Level(CompressionLevel.Fastest)); + + return app.Add("compression", service); + } + + private static async ValueTask AddHeader(IRequest request, IHandler content) + { + var response = await content.HandleAsync(request); + + response?.Headers.Add("Server", "genhttp"); + + return response; + } + +} \ No newline at end of file diff --git a/frameworks/genhttp/Tests/Baseline.cs b/frameworks/genhttp/Tests/Baseline.cs new file mode 100644 index 00000000..564caa6b --- /dev/null +++ b/frameworks/genhttp/Tests/Baseline.cs @@ -0,0 +1,16 @@ +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Baseline +{ + + [ResourceMethod] + public int Sum(int a, int b) => a + b; + + [ResourceMethod(RequestMethod.Post)] + public int Sum(int a, int b, [FromBody] int c) => a + b + c; + +} \ No newline at end of file diff --git a/frameworks/genhttp/Tests/Compression.cs b/frameworks/genhttp/Tests/Compression.cs new file mode 100644 index 00000000..664f6d2f --- /dev/null +++ b/frameworks/genhttp/Tests/Compression.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Compression +{ + private static FlexibleContentType _jsonType = FlexibleContentType.Get(ContentType.ApplicationJson); + + private static byte[]? _largeJsonBytes = LoadJson(); + + private static byte[]? LoadJson() + { + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var largePath = "/data/dataset-large.json"; + + if (File.Exists(largePath)) + { + var largeItems = JsonSerializer.Deserialize>(File.ReadAllText(largePath), jsonOptions); + + if (largeItems != null) + { + var processed = largeItems.Select(d => new ProcessedItem + { + Id = d.Id, Name = d.Name, Category = d.Category, + Price = d.Price, Quantity = d.Quantity, Active = d.Active, + Tags = d.Tags, Rating = d.Rating, + Total = Math.Round(d.Price * d.Quantity, 2) + }).ToList(); + + return JsonSerializer.SerializeToUtf8Bytes(new { items = processed, count = processed.Count }, jsonOptions); + } + } + + return null; + } + + [ResourceMethod] + public Result Compress() + { + if (_largeJsonBytes == null) + { + throw new ProviderException(ResponseStatus.InternalServerError, "No dataset"); + } + + return new Result(_largeJsonBytes).Type(_jsonType); + } + +} \ No newline at end of file diff --git a/frameworks/genhttp/Tests/Database.cs b/frameworks/genhttp/Tests/Database.cs new file mode 100644 index 00000000..2c84fa39 --- /dev/null +++ b/frameworks/genhttp/Tests/Database.cs @@ -0,0 +1,76 @@ +using System.Text.Json; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Webservices; + +using Microsoft.Data.Sqlite; + +namespace genhttp.Tests; + +public class Database +{ + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + private static readonly SqliteConnection? DbConn = OpenConnection(); + + private static SqliteConnection OpenConnection() + { + var dbPath = "/data/benchmark.db"; + + if (File.Exists(dbPath)) + { + var con = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); + con.Open(); + + using var pragma = con.CreateCommand(); + pragma.CommandText = "PRAGMA mmap_size=268435456"; + pragma.ExecuteNonQuery(); + + return con; + } + + return null; + } + + [ResourceMethod] + public ListWithCount Compute(int min = 10, int max = 50) + { + if (DbConn == null) + { + throw new ProviderException(ResponseStatus.InternalServerError, "DB not available"); + } + + using var cmd = DbConn.CreateCommand(); + cmd.CommandText = "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN @min AND @max LIMIT 50"; + cmd.Parameters.AddWithValue("@min", min); + cmd.Parameters.AddWithValue("@max", max); + + using var reader = cmd.ExecuteReader(); + + var items = new List(); + + while (reader.Read()) + { + items.Add(new ProcessedItem + { + Id = reader.GetInt32(0), + Name = reader.GetString(1), + Category = reader.GetString(2), + Price = reader.GetDouble(3), + Quantity = reader.GetInt32(4), + Active = reader.GetInt32(5) == 1, + Tags = JsonSerializer.Deserialize>(reader.GetString(6)), + Rating = new RatingInfo { Score = reader.GetDouble(7), Count = reader.GetInt32(8) }, + }); + } + + return new ListWithCount(items); + } + +} \ No newline at end of file diff --git a/frameworks/genhttp/Tests/Json.cs b/frameworks/genhttp/Tests/Json.cs new file mode 100644 index 00000000..09107bfa --- /dev/null +++ b/frameworks/genhttp/Tests/Json.cs @@ -0,0 +1,55 @@ +using System.Text.Json; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Json +{ + private static List? datasetItems = LoadItems(); + + private static List LoadItems() + { + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var datasetPath = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json"; + + if (File.Exists(datasetPath)) + { + return JsonSerializer.Deserialize>(File.ReadAllText(datasetPath), jsonOptions); + } + + return null; + } + + [ResourceMethod] + public ListWithCount Compute() + { + if (datasetItems == null) + { + throw new ProviderException(ResponseStatus.InternalServerError, "No dataset"); + } + + var processed = new List(datasetItems.Count); + + foreach (var d in datasetItems) + { + processed.Add(new ProcessedItem + { + Id = d.Id, Name = d.Name, Category = d.Category, + Price = d.Price, Quantity = d.Quantity, Active = d.Active, + Tags = d.Tags, Rating = d.Rating, + Total = Math.Round(d.Price * d.Quantity, 2) + }); + } + + return new(processed); + } + +} \ No newline at end of file diff --git a/frameworks/genhttp/Tests/Upload.cs b/frameworks/genhttp/Tests/Upload.cs new file mode 100644 index 00000000..0c285416 --- /dev/null +++ b/frameworks/genhttp/Tests/Upload.cs @@ -0,0 +1,38 @@ +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Upload +{ + + [ResourceMethod(RequestMethod.Post)] + public ValueTask Compute(Stream input) + { + if (input.CanSeek) + { + // internal engine + return ValueTask.FromResult(input.Length); + } + + // kestrel + return ComputeManually(input); + } + + private async ValueTask ComputeManually(Stream input) + { + var buffer = new byte[8192]; + + long total = 0; + + var read = 0; + + while ((read = await input.ReadAsync(buffer)) > 0) + { + total += read; + } + + return total; + } + +} \ No newline at end of file diff --git a/frameworks/genhttp/genhttp.csproj b/frameworks/genhttp/genhttp.csproj index a8e99f9b..2bd73c78 100644 --- a/frameworks/genhttp/genhttp.csproj +++ b/frameworks/genhttp/genhttp.csproj @@ -6,10 +6,11 @@ true - + + diff --git a/frameworks/genhttp/meta.json b/frameworks/genhttp/meta.json index 5a798e8a..b92ae624 100644 --- a/frameworks/genhttp/meta.json +++ b/frameworks/genhttp/meta.json @@ -1,9 +1,9 @@ { - "display_name": "genhttp", + "display_name": "GenHTTP", "language": "C#", "type": "framework", - "engine": "Kestrel", - "description": "Lightweight embeddable C# web server using the Kestrel engine for HTTP/1.1, HTTP/2, and HTTP/3 support.", + "engine": "GenHTTP", + "description": "Lightweight, embeddable and modular C# web server.", "repo": "https://github.com/Kaliumhexacyanoferrat/GenHTTP", "enabled": true, "tests": [ @@ -14,10 +14,6 @@ "upload", "compression", "noisy", - "mixed", - "baseline-h2", - "static-h2", - "baseline-h3", - "static-h3" + "mixed" ] }