复制代码
using MiniGin;
// 创建引擎(类似 gin.Default())
var app = Gin.Default();
// 启用 Swagger
app.UseSwagger("Mini Gin API", "v1");
// 全局中间件
app.Use(
Middleware.CORS(),
Middleware.RequestId()
);
// 根路由
app.GET("/", async ctx => await ctx.String(200, "Mini Gin is ready!"));
app.GET("/ping", async ctx => await ctx.JSON(new { message = "pong" }));
// API 分组
var api = app.Group("/api");
api.Use(ctx =>
{
ctx.Header("X-Api-Version", "1.0");
return Task.CompletedTask;
});
// RESTful 风格路由
api.GET("/users", async ctx =>
{
var page = ctx.Query<int>("page") ?? 1;
var size = ctx.Query<int>("size") ?? 10;
await ctx.JSON(new
{
users = new[] { new { id = 1, name = "Alice" }, new { id = 2, name = "Bob" } },
page,
size
});
});
api.GET("/users/:id", async ctx =>
{
var id = ctx.Param("id");
await ctx.JSON(new { id, name = $"User_{id}" });
});
api.POST("/users", async ctx =>
{
var user = await ctx.BindAsync<CreateUserRequest>();
if (user == null)
{
await ctx.BadRequest(new { error = "Invalid request body" });
return;
}
await ctx.Created(new { id = 1, name = user.Name, email = user.Email });
});
api.PUT("/users/:id", async ctx =>
{
var id = ctx.Param("id");
var user = await ctx.BindAsync<UpdateUserRequest>();
await ctx.OK(new { id, updated = true, name = user?.Name });
});
api.DELETE("/users/:id", async ctx =>
{
var id = ctx.Param("id");
await ctx.OK(new { id, deleted = true });
});
// 嵌套分组
var admin = api.Group("/admin");
admin.Use(Middleware.BasicAuth((user, pass) => user == "admin" && pass == "123456"));
admin.GET("/dashboard", async ctx =>
{
var user = ctx.Get<string>("user");
await ctx.JSON(new { message = $"Welcome {user}!", role = "admin" });
});
// 启动服务器
await app.Run("http://localhost:5000/");
// 请求模型
record CreateUserRequest(string Name, string Email);
record UpdateUserRequest(string? Name, string? Email);
1. Context
复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace MiniGin;
/// <summary>
/// 请求上下文 - 封装 HTTP 请求/响应的所有操作
/// </summary>
public sealed class Context
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly Dictionary<string, string> _params;
private readonly Dictionary<string, object> _items = new();
private bool _responseSent;
private string? _cachedBody;
internal Context(HttpListenerContext httpContext, Dictionary<string, string> routeParams, JsonSerializerOptions jsonOptions)
{
HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
_params = routeParams ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_jsonOptions = jsonOptions;
}
#region 基础属性
/// <summary>原始 HttpListenerContext</summary>
public HttpListenerContext HttpContext { get; }
/// <summary>HTTP 请求对象</summary>
public HttpListenerRequest Request => HttpContext.Request;
/// <summary>HTTP 响应对象</summary>
public HttpListenerResponse Response => HttpContext.Response;
/// <summary>请求路径</summary>
public string Path => Request.Url?.AbsolutePath ?? "/";
/// <summary>请求方法</summary>
public string Method => Request.HttpMethod ?? "GET";
/// <summary>完整 URL</summary>
public string FullUrl => Request.Url?.ToString() ?? "";
/// <summary>客户端 IP</summary>
public string ClientIP => Request.RemoteEndPoint?.Address?.ToString() ?? "";
/// <summary>Content-Type</summary>
public string? ContentType => Request.ContentType;
/// <summary>是否已中止</summary>
public bool IsAborted { get; private set; }
#endregion
#region 路由参数
/// <summary>获取路由参数</summary>
public string? Param(string key)
=> _params.TryGetValue(key, out var value) ? value : null;
/// <summary>获取路由参数(带默认值)</summary>
public string Param(string key, string defaultValue)
=> _params.TryGetValue(key, out var value) ? value : defaultValue;
/// <summary>获取所有路由参数</summary>
public IReadOnlyDictionary<string, string> Params => _params;
#endregion
#region 查询参数
/// <summary>获取查询参数</summary>
public string? Query(string key)
=> Request.QueryString[key];
/// <summary>获取查询参数(带默认值)</summary>
public string Query(string key, string defaultValue)
=> Request.QueryString[key] ?? defaultValue;
/// <summary>获取查询参数并转换类型</summary>
public T? Query<T>(string key) where T : struct
{
var value = Request.QueryString[key];
if (string.IsNullOrEmpty(value)) return null;
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return null;
}
}
/// <summary>获取所有查询参数的 key</summary>
public string[] QueryKeys => Request.QueryString.AllKeys!;
#endregion
#region 请求头
/// <summary>获取请求头</summary>
public string? GetHeader(string key)
=> Request.Headers[key];
/// <summary>获取请求头(带默认值)</summary>
public string GetHeader(string key, string defaultValue)
=> Request.Headers[key] ?? defaultValue;
#endregion
#region 请求体
/// <summary>读取原始请求体</summary>
public async Task<string> GetRawBodyAsync()
{
if (_cachedBody != null)
return _cachedBody;
if (!Request.HasEntityBody)
return _cachedBody = string.Empty;
using var reader = new StreamReader(Request.InputStream, Request.ContentEncoding ?? Encoding.UTF8);
return _cachedBody = await reader.ReadToEndAsync();
}
/// <summary>绑定 JSON 请求体到对象</summary>
public async Task<T?> BindAsync<T>() where T : class
{
var body = await GetRawBodyAsync();
if (string.IsNullOrWhiteSpace(body))
return null;
return JsonSerializer.Deserialize<T>(body, _jsonOptions);
}
/// <summary>绑定 JSON 请求体到对象(带默认值)</summary>
public async Task<T> BindAsync<T>(T defaultValue) where T : class
{
var result = await BindAsync<T>();
return result ?? defaultValue;
}
/// <summary>必须绑定成功,否则抛异常</summary>
public async Task<T> MustBindAsync<T>() where T : class
{
var result = await BindAsync<T>();
return result ?? throw new InvalidOperationException($"Failed to bind request body to {typeof(T).Name}");
}
#endregion
#region 上下文数据
/// <summary>设置上下文数据</summary>
public void Set(string key, object value) => _items[key] = value;
/// <summary>获取上下文数据</summary>
public T? Get<T>(string key) where T : class
=> _items.TryGetValue(key, out var value) ? value as T : null;
/// <summary>获取上下文数据(带默认值)</summary>
public T Get<T>(string key, T defaultValue) where T : class
=> _items.TryGetValue(key, out var value) && value is T typed ? typed : defaultValue;
/// <summary>是否存在上下文数据</summary>
public bool Has(string key) => _items.ContainsKey(key);
#endregion
#region 响应方法
/// <summary>中止请求处理</summary>
public void Abort() => IsAborted = true;
/// <summary>设置响应头</summary>
public Context Header(string key, string value)
{
Response.Headers[key] = value;
return this;
}
/// <summary>设置状态码并结束响应</summary>
public Task Status(int statusCode)
{
if (!TryStartResponse()) return Task.CompletedTask;
Response.StatusCode = statusCode;
Response.ContentLength64 = 0;
Response.OutputStream.Close();
return Task.CompletedTask;
}
/// <summary>返回纯文本</summary>
public Task String(int statusCode, string content)
{
if (!TryStartResponse()) return Task.CompletedTask;
var bytes = Encoding.UTF8.GetBytes(content);
Response.StatusCode = statusCode;
Response.ContentType = "text/plain; charset=utf-8";
Response.ContentLength64 = bytes.Length;
return WriteAndCloseAsync(bytes);
}
/// <summary>返回 HTML</summary>
public Task HTML(int statusCode, string html)
{
if (!TryStartResponse()) return Task.CompletedTask;
var bytes = Encoding.UTF8.GetBytes(html);
Response.StatusCode = statusCode;
Response.ContentType = "text/html; charset=utf-8";
Response.ContentLength64 = bytes.Length;
return WriteAndCloseAsync(bytes);
}
/// <summary>返回 JSON</summary>
public Task JSON(int statusCode, object? data)
{
if (!TryStartResponse()) return Task.CompletedTask;
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions);
Response.StatusCode = statusCode;
Response.ContentType = "application/json; charset=utf-8";
Response.ContentLength64 = bytes.Length;
return WriteAndCloseAsync(bytes);
}
/// <summary>返回 JSON(200 状态码)</summary>
public Task JSON(object? data) => JSON(200, data);
/// <summary>返回原始字节</summary>
public Task Data(int statusCode, string contentType, byte[] data)
{
if (!TryStartResponse()) return Task.CompletedTask;
Response.StatusCode = statusCode;
Response.ContentType = contentType;
Response.ContentLength64 = data.Length;
return WriteAndCloseAsync(data);
}
/// <summary>重定向</summary>
public Task Redirect(int statusCode, string location)
{
if (!TryStartResponse()) return Task.CompletedTask;
Response.StatusCode = statusCode;
Response.RedirectLocation = location;
Response.ContentLength64 = 0;
Response.OutputStream.Close();
return Task.CompletedTask;
}
/// <summary>重定向(302)</summary>
public Task Redirect(string location) => Redirect(302, location);
#endregion
#region 快捷响应方法
/// <summary>200 OK</summary>
public Task OK(object? data = null) => data == null ? Status(200) : JSON(200, data);
/// <summary>201 Created</summary>
public Task Created(object? data = null) => data == null ? Status(201) : JSON(201, data);
/// <summary>204 No Content</summary>
public Task NoContent() => Status(204);
/// <summary>400 Bad Request</summary>
public Task BadRequest(object? error = null)
=> JSON(400, error ?? new { error = "Bad Request" });
/// <summary>401 Unauthorized</summary>
public Task Unauthorized(object? error = null)
=> JSON(401, error ?? new { error = "Unauthorized" });
/// <summary>403 Forbidden</summary>
public Task Forbidden(object? error = null)
=> JSON(403, error ?? new { error = "Forbidden" });
/// <summary>404 Not Found</summary>
public Task NotFound(object? error = null)
=> JSON(404, error ?? new { error = "Not Found" });
/// <summary>500 Internal Server Error</summary>
public Task InternalServerError(object? error = null)
=> JSON(500, error ?? new { error = "Internal Server Error" });
/// <summary>中止并返回状态码</summary>
public Task AbortWithStatus(int statusCode)
{
Abort();
return Status(statusCode);
}
/// <summary>中止并返回 JSON 错误</summary>
public Task AbortWithJSON(int statusCode, object error)
{
Abort();
return JSON(statusCode, error);
}
#endregion
#region 私有方法
private bool TryStartResponse()
{
if (_responseSent) return false;
_responseSent = true;
return true;
}
private async Task WriteAndCloseAsync(byte[] bytes)
{
await Response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
Response.OutputStream.Close();
}
#endregion
}
2.Engine
复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace MiniGin;
/// <summary>
/// Gin 风格的 HTTP 引擎 - 核心入口
/// </summary>
public class Engine : RouterGroup
{
private readonly List<Route> _routes = new();
private readonly JsonSerializerOptions _jsonOptions;
private HttpListener? _listener;
private bool _swaggerEnabled;
private string _swaggerTitle = "MiniGin API";
private string _swaggerVersion = "v1";
/// <summary>
/// 创建新的引擎实例
/// </summary>
public Engine() : this(new JsonSerializerOptions(JsonSerializerDefaults.Web))
{
}
/// <summary>
/// 创建新的引擎实例(自定义 JSON 选项)
/// </summary>
public Engine(JsonSerializerOptions jsonOptions) : base(null!, "")
{
_jsonOptions = jsonOptions;
SetEngine(this);
}
private void SetEngine(Engine engine)
{
var field = typeof(RouterGroup).GetField("_engine", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
field?.SetValue(this, engine);
}
#region 配置
/// <summary>
/// 启用 Swagger UI 和 OpenAPI 文档
/// </summary>
/// <param name="title">API 标题</param>
/// <param name="version">API 版本</param>
public Engine UseSwagger(string title = "MiniGin API", string version = "v1")
{
_swaggerEnabled = true;
_swaggerTitle = title;
_swaggerVersion = version;
return this;
}
/// <summary>
/// 获取所有已注册的路由
/// </summary>
public IReadOnlyList<Route> Routes => _routes;
/// <summary>
/// JSON 序列化选项
/// </summary>
public JsonSerializerOptions JsonOptions => _jsonOptions;
#endregion
#region 路由注册(内部)
internal void AddRoute(string method, string path, HandlerFunc[] handlers)
{
if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("HTTP method is required.", nameof(method));
if (handlers == null || handlers.Length == 0)
throw new ArgumentException("At least one handler is required.", nameof(handlers));
var pattern = RoutePattern.Parse(path);
var route = new Route(method.ToUpperInvariant(), path, pattern, handlers);
_routes.Add(route);
_routes.Sort((a, b) => b.Pattern.LiteralCount.CompareTo(a.Pattern.LiteralCount));
}
#endregion
#region 运行
/// <summary>
/// 启动 HTTP 服务器
/// </summary>
/// <param name="address">监听地址,如 http://localhost:5000/</param>
public Task Run(string address = "http://localhost:5000/")
=> Run(address, CancellationToken.None);
/// <summary>
/// 启动 HTTP 服务器(支持取消)
/// </summary>
/// <param name="address">监听地址</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task Run(string address, CancellationToken cancellationToken)
{
if (!address.EndsWith("/"))
address += "/";
_listener = new HttpListener();
_listener.Prefixes.Add(address);
_listener.Start();
Console.WriteLine($"[MiniGin] Listening on {address}");
if (_swaggerEnabled)
Console.WriteLine($"[MiniGin] Swagger UI: {address}swagger");
try
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
var httpContext = await _listener.GetContextAsync();
_ = Task.Run(() => HandleRequestAsync(httpContext), cancellationToken);
}
catch (Exception ex) when (!(ex is HttpListenerException))
{
Console.WriteLine($"[MiniGin] Error accepting connection: {ex.Message}");
}
}
}
catch (HttpListenerException) when (cancellationToken.IsCancellationRequested)
{
// 正常关闭
}
catch (Exception ex)
{
Console.WriteLine($"[MiniGin] Fatal error: {ex.Message}");
Console.WriteLine(ex.StackTrace);
throw;
}
finally
{
_listener.Stop();
_listener.Close();
}
}
/// <summary>
/// 停止服务器
/// </summary>
public void Stop()
{
_listener?.Stop();
}
private async Task HandleRequestAsync(HttpListenerContext httpContext)
{
try
{
var path = httpContext.Request.Url?.AbsolutePath ?? "/";
var method = httpContext.Request.HttpMethod ?? "GET";
// 处理 Swagger
if (_swaggerEnabled && await TryHandleSwaggerAsync(httpContext, path))
return;
// 查找路由
var (route, routeParams) = FindRoute(method, path);
if (route == null)
{
await WriteNotFound(httpContext.Response);
return;
}
// 创建上下文
var ctx = new Context(httpContext, routeParams, _jsonOptions);
// 执行处理器链
await ExecuteHandlers(ctx, route.Handlers);
}
catch (Exception ex)
{
await WriteError(httpContext.Response, ex);
}
}
private async Task ExecuteHandlers(Context ctx, HandlerFunc[] handlers)
{
foreach (var handler in handlers)
{
if (ctx.IsAborted)
break;
await handler(ctx);
}
}
private (Route? route, Dictionary<string, string> routeParams) FindRoute(string method, string path)
{
foreach (var route in _routes)
{
if (!string.Equals(route.Method, method, StringComparison.OrdinalIgnoreCase))
continue;
if (route.Pattern.TryMatch(path, out var routeParams))
return (route, routeParams);
}
return (null, new Dictionary<string, string>());
}
#endregion
#region Swagger
private async Task<bool> TryHandleSwaggerAsync(HttpListenerContext context, string path)
{
if (path.Equals("/swagger", StringComparison.OrdinalIgnoreCase) ||
path.Equals("/swagger/", StringComparison.OrdinalIgnoreCase))
{
var html = GenerateSwaggerHtml();
await WriteResponse(context.Response, 200, "text/html; charset=utf-8", html);
return true;
}
if (path.Equals("/swagger/v1/swagger.json", StringComparison.OrdinalIgnoreCase))
{
var doc = GenerateOpenApiDoc();
var json = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
await WriteResponse(context.Response, 200, "application/json; charset=utf-8", json);
return true;
}
return false;
}
private object GenerateOpenApiDoc()
{
var paths = new Dictionary<string, object>();
foreach (var routeGroup in _routes.GroupBy(r => r.OpenApiPath))
{
var operations = new Dictionary<string, object>();
foreach (var route in routeGroup)
{
operations[route.Method.ToLowerInvariant()] = new
{
operationId = $"{route.Method}_{route.Path.Replace("/", "_").Replace(":", "")}",
parameters = route.PathParameters.Select(p => new
{
name = p,
@in = "path",
required = true,
schema = new { type = "string" }
}).ToArray(),
responses = new Dictionary<string, object>
{
["200"] = new { description = "OK" }
}
};
}
paths[routeGroup.Key] = operations;
}
return new
{
openapi = "3.0.1",
info = new { title = _swaggerTitle, version = _swaggerVersion },
paths
};
}
private static string GenerateSwaggerHtml() => @"<!doctype html>
<html>
<head>
<meta charset=""utf-8"" />
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
<title>Swagger UI</title>
<link rel=""stylesheet"" href=""https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"" />
</head>
<body>
<div id=""swagger-ui""></div>
<script src=""https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js""></script>
<script>
window.onload = () => {
SwaggerUIBundle({
url: '/swagger/v1/swagger.json',
dom_id: '#swagger-ui',
});
};
</script>
</body>
</html>";
#endregion
#region 响应辅助
private static async Task WriteResponse(HttpListenerResponse response, int statusCode, string contentType, string body)
{
var bytes = Encoding.UTF8.GetBytes(body);
response.StatusCode = statusCode;
response.ContentType = contentType;
response.ContentLength64 = bytes.Length;
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
response.OutputStream.Close();
}
private static Task WriteNotFound(HttpListenerResponse response)
=> WriteResponse(response, 404, "application/json", "{\"error\":\"Not Found\"}");
private static Task WriteError(HttpListenerResponse response, Exception ex)
=> WriteResponse(response, 500, "application/json", $"{{\"error\":\"{ex.Message.Replace("\"", "\\\"")}\"}}");
#endregion
}
3.Gin
复制代码
namespace MiniGin;
/// <summary>
/// MiniGin 工厂方法
/// </summary>
public static class Gin
{
/// <summary>
/// 创建默认引擎(包含 Logger 和 Recovery 中间件)
/// </summary>
public static Engine Default()
{
var engine = new Engine();
engine.Use(Middleware.Logger(), Middleware.Recovery());
return engine;
}
/// <summary>
/// 创建空白引擎(不包含任何中间件)
/// </summary>
public static Engine New()
{
return new Engine();
}
}
4.Interface
复制代码
namespace MiniGin;
/// <summary>
/// MiniGin 工厂方法
/// </summary>
public static class Gin
{
/// <summary>
/// 创建默认引擎(包含 Logger 和 Recovery 中间件)
/// </summary>
public static Engine Default()
{
var engine = new Engine();
engine.Use(Middleware.Logger(), Middleware.Recovery());
return engine;
}
/// <summary>
/// 创建空白引擎(不包含任何中间件)
/// </summary>
public static Engine New()
{
return new Engine();
}
}
5.Middleware
复制代码
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace MiniGin;
/// <summary>
/// 内置中间件集合
/// </summary>
public static class Middleware
{
/// <summary>
/// 请求日志中间件
/// </summary>
/// <param name="logger">自定义日志输出(默认 Console.WriteLine)</param>
public static HandlerFunc Logger(Action<string>? logger = null)
{
logger ??= Console.WriteLine;
return ctx =>
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
logger($"[{timestamp}] {ctx.Method} {ctx.Path} from {ctx.ClientIP}");
return Task.CompletedTask;
};
}
/// <summary>
/// 请求计时中间件
/// </summary>
/// <param name="callback">计时回调</param>
public static HandlerFunc Timer(Action<Context, long>? callback = null)
{
callback ??= (ctx, ms) => Console.WriteLine($"[Timer] {ctx.Method} {ctx.Path} - {ms}ms");
return ctx =>
{
var sw = Stopwatch.StartNew();
ctx.Set("__timer_start", sw);
ctx.Set("__timer_callback", (Action)(() =>
{
sw.Stop();
callback(ctx, sw.ElapsedMilliseconds);
}));
return Task.CompletedTask;
};
}
/// <summary>
/// 错误恢复中间件
/// </summary>
/// <param name="showStackTrace">是否显示堆栈跟踪</param>
public static HandlerFunc Recovery(bool showStackTrace = false)
{
return async ctx =>
{
try
{
// 预留用于自定义错误处理
}
catch (Exception ex)
{
var message = showStackTrace ? ex.ToString() : ex.Message;
await ctx.JSON(500, new
{
error = true,
message,
timestamp = DateTime.UtcNow
});
ctx.Abort();
}
};
}
/// <summary>
/// CORS 中间件
/// </summary>
/// <param name="config">CORS 配置</param>
public static HandlerFunc CORS(CorsConfig? config = null)
{
config ??= new CorsConfig();
return async ctx =>
{
ctx.Header("Access-Control-Allow-Origin", config.AllowOrigins)
.Header("Access-Control-Allow-Methods", config.AllowMethods)
.Header("Access-Control-Allow-Headers", config.AllowHeaders);
if (config.AllowCredentials)
ctx.Header("Access-Control-Allow-Credentials", "true");
if (config.MaxAge > 0)
ctx.Header("Access-Control-Max-Age", config.MaxAge.ToString());
// 预检请求直接返回
if (ctx.Method == "OPTIONS")
{
await ctx.Status(204);
ctx.Abort();
}
};
}
/// <summary>
/// HTTP Basic 认证中间件
/// </summary>
/// <param name="validator">用户名密码验证器</param>
/// <param name="realm">认证域</param>
public static HandlerFunc BasicAuth(Func<string, string, bool> validator, string realm = "Authorization Required")
{
return async ctx =>
{
var authHeader = ctx.GetHeader("Authorization");
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
{
ctx.Header("WWW-Authenticate", $"Basic realm=\"{realm}\"");
await ctx.Unauthorized(new { error = "Unauthorized" });
ctx.Abort();
return;
}
try
{
var encoded = authHeader["Basic ".Length..];
var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
var parts = decoded.Split(':', 2);
if (parts.Length != 2 || !validator(parts[0], parts[1]))
{
await ctx.Unauthorized(new { error = "Invalid credentials" });
ctx.Abort();
}
else
{
ctx.Set("user", parts[0]);
}
}
catch
{
await ctx.Unauthorized(new { error = "Invalid authorization header" });
ctx.Abort();
}
};
}
/// <summary>
/// API Key 认证中间件
/// </summary>
/// <param name="headerName">请求头名称</param>
/// <param name="validator">API Key 验证器</param>
public static HandlerFunc ApiKey(string headerName, Func<string?, bool> validator)
{
return async ctx =>
{
var apiKey = ctx.GetHeader(headerName);
if (!validator(apiKey))
{
await ctx.Unauthorized(new { error = "Invalid API Key" });
ctx.Abort();
}
};
}
/// <summary>
/// 请求 ID 中间件
/// </summary>
/// <param name="headerName">请求头名称</param>
public static HandlerFunc RequestId(string headerName = "X-Request-ID")
{
return ctx =>
{
var requestId = ctx.GetHeader(headerName);
if (string.IsNullOrEmpty(requestId))
requestId = Guid.NewGuid().ToString("N");
ctx.Set("RequestId", requestId);
ctx.Header(headerName, requestId);
return Task.CompletedTask;
};
}
/// <summary>
/// 自定义响应头中间件
/// </summary>
/// <param name="headers">响应头键值对</param>
public static HandlerFunc Headers(params (string key, string value)[] headers)
{
return ctx =>
{
foreach (var (key, value) in headers)
ctx.Header(key, value);
return Task.CompletedTask;
};
}
/// <summary>
/// 静态文件中间件(简单实现)
/// </summary>
/// <param name="urlPrefix">URL 前缀</param>
/// <param name="rootPath">文件系统根路径</param>
public static HandlerFunc Static(string urlPrefix, string rootPath)
{
return async ctx =>
{
if (!ctx.Path.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
return;
var relativePath = ctx.Path[urlPrefix.Length..].TrimStart('/');
var filePath = System.IO.Path.Combine(rootPath, relativePath);
if (!System.IO.File.Exists(filePath))
{
await ctx.NotFound();
ctx.Abort();
return;
}
var contentType = GetContentType(filePath);
var bytes = await System.IO.File.ReadAllBytesAsync(filePath);
await ctx.Data(200, contentType, bytes);
ctx.Abort();
};
}
private static string GetContentType(string filePath)
{
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".html" or ".htm" => "text/html; charset=utf-8",
".css" => "text/css; charset=utf-8",
".js" => "application/javascript; charset=utf-8",
".json" => "application/json; charset=utf-8",
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".svg" => "image/svg+xml",
".ico" => "image/x-icon",
".woff" => "font/woff",
".woff2" => "font/woff2",
".ttf" => "font/ttf",
".pdf" => "application/pdf",
".xml" => "application/xml",
_ => "application/octet-stream"
};
}
}
/// <summary>
/// CORS 配置
/// </summary>
public class CorsConfig
{
/// <summary>允许的源</summary>
public string AllowOrigins { get; set; } = "*";
/// <summary>允许的方法</summary>
public string AllowMethods { get; set; } = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
/// <summary>允许的请求头</summary>
public string AllowHeaders { get; set; } = "Content-Type, Authorization, X-Requested-With";
/// <summary>是否允许携带凭据</summary>
public bool AllowCredentials { get; set; } = false;
/// <summary>预检请求缓存时间(秒)</summary>
public int MaxAge { get; set; } = 86400;
}
6.Route
复制代码
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiniGin;
/// <summary>
/// 路由定义
/// </summary>
public sealed class Route
{
/// <summary>
/// 创建路由定义
/// </summary>
public Route(string method, string path, RoutePattern pattern, HandlerFunc[] handlers)
{
Method = method;
Path = path;
Pattern = pattern;
Handlers = handlers;
}
/// <summary>HTTP 方法</summary>
public string Method { get; }
/// <summary>路由路径</summary>
public string Path { get; }
/// <summary>路由模式</summary>
public RoutePattern Pattern { get; }
/// <summary>处理器链</summary>
public HandlerFunc[] Handlers { get; }
/// <summary>OpenAPI 格式路径</summary>
public string OpenApiPath => Path.Split('/')
.Select(s => s.StartsWith(":") ? "{" + s[1..] + "}" : s)
.Aggregate((a, b) => a + "/" + b);
/// <summary>路径参数列表</summary>
public string[] PathParameters => Path.Split('/')
.Where(s => s.StartsWith(":"))
.Select(s => s[1..])
.ToArray();
}
/// <summary>
/// 路由模式解析
/// </summary>
public sealed class RoutePattern
{
private readonly Segment[] _segments;
private RoutePattern(Segment[] segments) => _segments = segments;
/// <summary>
/// 解析路由模式
/// </summary>
public static RoutePattern Parse(string path)
{
var cleaned = (path ?? "/").Trim().Trim('/');
if (string.IsNullOrEmpty(cleaned))
return new RoutePattern(Array.Empty<Segment>());
var parts = cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
var segments = parts.Select(ParseSegment).ToArray();
return new RoutePattern(segments);
}
private static Segment ParseSegment(string part)
{
if (part.StartsWith(":"))
return new Segment(true, part[1..], false);
if (part.StartsWith("*"))
return new Segment(true, part[1..], true);
return new Segment(false, part, false);
}
/// <summary>
/// 尝试匹配请求路径
/// </summary>
public bool TryMatch(string requestPath, out Dictionary<string, string> routeParams)
{
routeParams = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var cleaned = (requestPath ?? "/").Trim().Trim('/');
var parts = string.IsNullOrEmpty(cleaned)
? Array.Empty<string>()
: cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
// 检查通配符
var hasWildcard = _segments.Any(s => s.IsWildcard);
if (!hasWildcard && parts.Length != _segments.Length)
return false;
for (var i = 0; i < _segments.Length; i++)
{
var segment = _segments[i];
if (segment.IsWildcard)
{
// 通配符匹配剩余所有路径
var remaining = string.Join("/", parts.Skip(i));
routeParams[segment.Value] = Uri.UnescapeDataString(remaining);
return true;
}
if (i >= parts.Length)
return false;
var value = parts[i];
if (segment.IsParam)
{
routeParams[segment.Value] = Uri.UnescapeDataString(value);
}
else if (!string.Equals(segment.Value, value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
/// <summary>字面量段数量(用于排序)</summary>
public int LiteralCount => _segments.Count(s => !s.IsParam);
private readonly record struct Segment(bool IsParam, string Value, bool IsWildcard = false);
}
7.RouteGroup
复制代码
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiniGin;
/// <summary>
/// 路由定义
/// </summary>
public sealed class Route
{
/// <summary>
/// 创建路由定义
/// </summary>
public Route(string method, string path, RoutePattern pattern, HandlerFunc[] handlers)
{
Method = method;
Path = path;
Pattern = pattern;
Handlers = handlers;
}
/// <summary>HTTP 方法</summary>
public string Method { get; }
/// <summary>路由路径</summary>
public string Path { get; }
/// <summary>路由模式</summary>
public RoutePattern Pattern { get; }
/// <summary>处理器链</summary>
public HandlerFunc[] Handlers { get; }
/// <summary>OpenAPI 格式路径</summary>
public string OpenApiPath => Path.Split('/')
.Select(s => s.StartsWith(":") ? "{" + s[1..] + "}" : s)
.Aggregate((a, b) => a + "/" + b);
/// <summary>路径参数列表</summary>
public string[] PathParameters => Path.Split('/')
.Where(s => s.StartsWith(":"))
.Select(s => s[1..])
.ToArray();
}
/// <summary>
/// 路由模式解析
/// </summary>
public sealed class RoutePattern
{
private readonly Segment[] _segments;
private RoutePattern(Segment[] segments) => _segments = segments;
/// <summary>
/// 解析路由模式
/// </summary>
public static RoutePattern Parse(string path)
{
var cleaned = (path ?? "/").Trim().Trim('/');
if (string.IsNullOrEmpty(cleaned))
return new RoutePattern(Array.Empty<Segment>());
var parts = cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
var segments = parts.Select(ParseSegment).ToArray();
return new RoutePattern(segments);
}
private static Segment ParseSegment(string part)
{
if (part.StartsWith(":"))
return new Segment(true, part[1..], false);
if (part.StartsWith("*"))
return new Segment(true, part[1..], true);
return new Segment(false, part, false);
}
/// <summary>
/// 尝试匹配请求路径
/// </summary>
public bool TryMatch(string requestPath, out Dictionary<string, string> routeParams)
{
routeParams = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var cleaned = (requestPath ?? "/").Trim().Trim('/');
var parts = string.IsNullOrEmpty(cleaned)
? Array.Empty<string>()
: cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
// 检查通配符
var hasWildcard = _segments.Any(s => s.IsWildcard);
if (!hasWildcard && parts.Length != _segments.Length)
return false;
for (var i = 0; i < _segments.Length; i++)
{
var segment = _segments[i];
if (segment.IsWildcard)
{
// 通配符匹配剩余所有路径
var remaining = string.Join("/", parts.Skip(i));
routeParams[segment.Value] = Uri.UnescapeDataString(remaining);
return true;
}
if (i >= parts.Length)
return false;
var value = parts[i];
if (segment.IsParam)
{
routeParams[segment.Value] = Uri.UnescapeDataString(value);
}
else if (!string.Equals(segment.Value, value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
/// <summary>字面量段数量(用于排序)</summary>
public int LiteralCount => _segments.Count(s => !s.IsParam);
private readonly record struct Segment(bool IsParam, string Value, bool IsWildcard = false);
}
8.MiniHttpApi.csproj
复制代码
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- 排除 MiniGin 子项目的源文件 -->
<ItemGroup>
<Compile Remove="MiniGin\**\*.cs" />
<None Remove="MiniGin\**\*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="MiniGin\MiniGin.csproj" />
</ItemGroup>
</Project>
9.MiniGin.csproj
复制代码
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- 包信息 -->
<PackageId>MiniGin</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Company>Your Company</Company>
<Description>A lightweight Gin-style HTTP framework for .NET based on HttpListener</Description>
<PackageTags>http;web;framework;gin;api;rest</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- 生成文档 -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<!-- 根命名空间 -->
<RootNamespace>MiniGin</RootNamespace>
<AssemblyName>MiniGin</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>