- 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>
-
-
-
- </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>
-
- <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>
- 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>
- MiniGin</AssemblyName>
- </PropertyGroup>
- <ItemGroup>
- <None Include="README.md" Pack="true" PackagePath="" />
- </ItemGroup>
- </Project>
复制代码 10.source
exercisebook/custom-webapi at main · liuzhixin405/exercisebook
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |