找回密码
 立即注册
首页 业界区 安全 用C#重现Gin风格:极简、效率与可扩展性设计笔记 ...

用C#重现Gin风格:极简、效率与可扩展性设计笔记

站竣凰 昨天 09:00
  1. using MiniGin;
  2. // 创建引擎(类似 gin.Default())
  3. var app = Gin.Default();
  4. // 启用 Swagger
  5. app.UseSwagger("Mini Gin API", "v1");
  6. // 全局中间件
  7. app.Use(
  8.     Middleware.CORS(),
  9.     Middleware.RequestId()
  10. );
  11. // 根路由
  12. app.GET("/", async ctx => await ctx.String(200, "Mini Gin is ready!"));
  13. app.GET("/ping", async ctx => await ctx.JSON(new { message = "pong" }));
  14. // API 分组
  15. var api = app.Group("/api");
  16. api.Use(ctx =>
  17. {
  18.     ctx.Header("X-Api-Version", "1.0");
  19.     return Task.CompletedTask;
  20. });
  21. // RESTful 风格路由
  22. api.GET("/users", async ctx =>
  23. {
  24.     var page = ctx.Query<int>("page") ?? 1;
  25.     var size = ctx.Query<int>("size") ?? 10;
  26.     await ctx.JSON(new
  27.     {
  28.         users = new[] { new { id = 1, name = "Alice" }, new { id = 2, name = "Bob" } },
  29.         page,
  30.         size
  31.     });
  32. });
  33. api.GET("/users/:id", async ctx =>
  34. {
  35.     var id = ctx.Param("id");
  36.     await ctx.JSON(new { id, name = $"User_{id}" });
  37. });
  38. api.POST("/users", async ctx =>
  39. {
  40.     var user = await ctx.BindAsync<CreateUserRequest>();
  41.     if (user == null)
  42.     {
  43.         await ctx.BadRequest(new { error = "Invalid request body" });
  44.         return;
  45.     }
  46.     await ctx.Created(new { id = 1, name = user.Name, email = user.Email });
  47. });
  48. api.PUT("/users/:id", async ctx =>
  49. {
  50.     var id = ctx.Param("id");
  51.     var user = await ctx.BindAsync<UpdateUserRequest>();
  52.     await ctx.OK(new { id, updated = true, name = user?.Name });
  53. });
  54. api.DELETE("/users/:id", async ctx =>
  55. {
  56.     var id = ctx.Param("id");
  57.     await ctx.OK(new { id, deleted = true });
  58. });
  59. // 嵌套分组
  60. var admin = api.Group("/admin");
  61. admin.Use(Middleware.BasicAuth((user, pass) => user == "admin" && pass == "123456"));
  62. admin.GET("/dashboard", async ctx =>
  63. {
  64.     var user = ctx.Get<string>("user");
  65.     await ctx.JSON(new { message = $"Welcome {user}!", role = "admin" });
  66. });
  67. // 启动服务器
  68. await app.Run("http://localhost:5000/");
  69. // 请求模型
  70. record CreateUserRequest(string Name, string Email);
  71. record UpdateUserRequest(string? Name, string? Email);
复制代码
1. Context
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Text;
  6. using System.Text.Json;
  7. using System.Threading.Tasks;
  8. namespace MiniGin;
  9. /// <summary>
  10. /// 请求上下文 - 封装 HTTP 请求/响应的所有操作
  11. /// </summary>
  12. public sealed class Context
  13. {
  14.     private readonly JsonSerializerOptions _jsonOptions;
  15.     private readonly Dictionary<string, string> _params;
  16.     private readonly Dictionary<string, object> _items = new();
  17.     private bool _responseSent;
  18.     private string? _cachedBody;
  19.     internal Context(HttpListenerContext httpContext, Dictionary<string, string> routeParams, JsonSerializerOptions jsonOptions)
  20.     {
  21.         HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
  22.         _params = routeParams ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  23.         _jsonOptions = jsonOptions;
  24.     }
  25.     #region 基础属性
  26.     /// <summary>原始 HttpListenerContext</summary>
  27.     public HttpListenerContext HttpContext { get; }
  28.     /// <summary>HTTP 请求对象</summary>
  29.     public HttpListenerRequest Request => HttpContext.Request;
  30.     /// <summary>HTTP 响应对象</summary>
  31.     public HttpListenerResponse Response => HttpContext.Response;
  32.     /// <summary>请求路径</summary>
  33.     public string Path => Request.Url?.AbsolutePath ?? "/";
  34.     /// <summary>请求方法</summary>
  35.     public string Method => Request.HttpMethod ?? "GET";
  36.     /// <summary>完整 URL</summary>
  37.     public string FullUrl => Request.Url?.ToString() ?? "";
  38.     /// <summary>客户端 IP</summary>
  39.     public string ClientIP => Request.RemoteEndPoint?.Address?.ToString() ?? "";
  40.     /// <summary>Content-Type</summary>
  41.     public string? ContentType => Request.ContentType;
  42.     /// <summary>是否已中止</summary>
  43.     public bool IsAborted { get; private set; }
  44.     #endregion
  45.     #region 路由参数
  46.     /// <summary>获取路由参数</summary>
  47.     public string? Param(string key)
  48.         => _params.TryGetValue(key, out var value) ? value : null;
  49.     /// <summary>获取路由参数(带默认值)</summary>
  50.     public string Param(string key, string defaultValue)
  51.         => _params.TryGetValue(key, out var value) ? value : defaultValue;
  52.     /// <summary>获取所有路由参数</summary>
  53.     public IReadOnlyDictionary<string, string> Params => _params;
  54.     #endregion
  55.     #region 查询参数
  56.     /// <summary>获取查询参数</summary>
  57.     public string? Query(string key)
  58.         => Request.QueryString[key];
  59.     /// <summary>获取查询参数(带默认值)</summary>
  60.     public string Query(string key, string defaultValue)
  61.         => Request.QueryString[key] ?? defaultValue;
  62.     /// <summary>获取查询参数并转换类型</summary>
  63.     public T? Query<T>(string key) where T : struct
  64.     {
  65.         var value = Request.QueryString[key];
  66.         if (string.IsNullOrEmpty(value)) return null;
  67.         try
  68.         {
  69.             return (T)Convert.ChangeType(value, typeof(T));
  70.         }
  71.         catch
  72.         {
  73.             return null;
  74.         }
  75.     }
  76.     /// <summary>获取所有查询参数的 key</summary>
  77.     public string[] QueryKeys => Request.QueryString.AllKeys!;
  78.     #endregion
  79.     #region 请求头
  80.     /// <summary>获取请求头</summary>
  81.     public string? GetHeader(string key)
  82.         => Request.Headers[key];
  83.     /// <summary>获取请求头(带默认值)</summary>
  84.     public string GetHeader(string key, string defaultValue)
  85.         => Request.Headers[key] ?? defaultValue;
  86.     #endregion
  87.     #region 请求体
  88.     /// <summary>读取原始请求体</summary>
  89.     public async Task<string> GetRawBodyAsync()
  90.     {
  91.         if (_cachedBody != null)
  92.             return _cachedBody;
  93.         if (!Request.HasEntityBody)
  94.             return _cachedBody = string.Empty;
  95.         using var reader = new StreamReader(Request.InputStream, Request.ContentEncoding ?? Encoding.UTF8);
  96.         return _cachedBody = await reader.ReadToEndAsync();
  97.     }
  98.     /// <summary>绑定 JSON 请求体到对象</summary>
  99.     public async Task<T?> BindAsync<T>() where T : class
  100.     {
  101.         var body = await GetRawBodyAsync();
  102.         if (string.IsNullOrWhiteSpace(body))
  103.             return null;
  104.         return JsonSerializer.Deserialize<T>(body, _jsonOptions);
  105.     }
  106.     /// <summary>绑定 JSON 请求体到对象(带默认值)</summary>
  107.     public async Task<T> BindAsync<T>(T defaultValue) where T : class
  108.     {
  109.         var result = await BindAsync<T>();
  110.         return result ?? defaultValue;
  111.     }
  112.     /// <summary>必须绑定成功,否则抛异常</summary>
  113.     public async Task<T> MustBindAsync<T>() where T : class
  114.     {
  115.         var result = await BindAsync<T>();
  116.         return result ?? throw new InvalidOperationException($"Failed to bind request body to {typeof(T).Name}");
  117.     }
  118.     #endregion
  119.     #region 上下文数据
  120.     /// <summary>设置上下文数据</summary>
  121.     public void Set(string key, object value) => _items[key] = value;
  122.     /// <summary>获取上下文数据</summary>
  123.     public T? Get<T>(string key) where T : class
  124.         => _items.TryGetValue(key, out var value) ? value as T : null;
  125.     /// <summary>获取上下文数据(带默认值)</summary>
  126.     public T Get<T>(string key, T defaultValue) where T : class
  127.         => _items.TryGetValue(key, out var value) && value is T typed ? typed : defaultValue;
  128.     /// <summary>是否存在上下文数据</summary>
  129.     public bool Has(string key) => _items.ContainsKey(key);
  130.     #endregion
  131.     #region 响应方法
  132.     /// <summary>中止请求处理</summary>
  133.     public void Abort() => IsAborted = true;
  134.     /// <summary>设置响应头</summary>
  135.     public Context Header(string key, string value)
  136.     {
  137.         Response.Headers[key] = value;
  138.         return this;
  139.     }
  140.     /// <summary>设置状态码并结束响应</summary>
  141.     public Task Status(int statusCode)
  142.     {
  143.         if (!TryStartResponse()) return Task.CompletedTask;
  144.         Response.StatusCode = statusCode;
  145.         Response.ContentLength64 = 0;
  146.         Response.OutputStream.Close();
  147.         return Task.CompletedTask;
  148.     }
  149.     /// <summary>返回纯文本</summary>
  150.     public Task String(int statusCode, string content)
  151.     {
  152.         if (!TryStartResponse()) return Task.CompletedTask;
  153.         var bytes = Encoding.UTF8.GetBytes(content);
  154.         Response.StatusCode = statusCode;
  155.         Response.ContentType = "text/plain; charset=utf-8";
  156.         Response.ContentLength64 = bytes.Length;
  157.         return WriteAndCloseAsync(bytes);
  158.     }
  159.     /// <summary>返回 HTML</summary>
  160.     public Task HTML(int statusCode, string html)
  161.     {
  162.         if (!TryStartResponse()) return Task.CompletedTask;
  163.         var bytes = Encoding.UTF8.GetBytes(html);
  164.         Response.StatusCode = statusCode;
  165.         Response.ContentType = "text/html; charset=utf-8";
  166.         Response.ContentLength64 = bytes.Length;
  167.         return WriteAndCloseAsync(bytes);
  168.     }
  169.     /// <summary>返回 JSON</summary>
  170.     public Task JSON(int statusCode, object? data)
  171.     {
  172.         if (!TryStartResponse()) return Task.CompletedTask;
  173.         var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions);
  174.         Response.StatusCode = statusCode;
  175.         Response.ContentType = "application/json; charset=utf-8";
  176.         Response.ContentLength64 = bytes.Length;
  177.         return WriteAndCloseAsync(bytes);
  178.     }
  179.     /// <summary>返回 JSON(200 状态码)</summary>
  180.     public Task JSON(object? data) => JSON(200, data);
  181.     /// <summary>返回原始字节</summary>
  182.     public Task Data(int statusCode, string contentType, byte[] data)
  183.     {
  184.         if (!TryStartResponse()) return Task.CompletedTask;
  185.         Response.StatusCode = statusCode;
  186.         Response.ContentType = contentType;
  187.         Response.ContentLength64 = data.Length;
  188.         return WriteAndCloseAsync(data);
  189.     }
  190.     /// <summary>重定向</summary>
  191.     public Task Redirect(int statusCode, string location)
  192.     {
  193.         if (!TryStartResponse()) return Task.CompletedTask;
  194.         Response.StatusCode = statusCode;
  195.         Response.RedirectLocation = location;
  196.         Response.ContentLength64 = 0;
  197.         Response.OutputStream.Close();
  198.         return Task.CompletedTask;
  199.     }
  200.     /// <summary>重定向(302)</summary>
  201.     public Task Redirect(string location) => Redirect(302, location);
  202.     #endregion
  203.     #region 快捷响应方法
  204.     /// <summary>200 OK</summary>
  205.     public Task OK(object? data = null) => data == null ? Status(200) : JSON(200, data);
  206.     /// <summary>201 Created</summary>
  207.     public Task Created(object? data = null) => data == null ? Status(201) : JSON(201, data);
  208.     /// <summary>204 No Content</summary>
  209.     public Task NoContent() => Status(204);
  210.     /// <summary>400 Bad Request</summary>
  211.     public Task BadRequest(object? error = null)
  212.         => JSON(400, error ?? new { error = "Bad Request" });
  213.     /// <summary>401 Unauthorized</summary>
  214.     public Task Unauthorized(object? error = null)
  215.         => JSON(401, error ?? new { error = "Unauthorized" });
  216.     /// <summary>403 Forbidden</summary>
  217.     public Task Forbidden(object? error = null)
  218.         => JSON(403, error ?? new { error = "Forbidden" });
  219.     /// <summary>404 Not Found</summary>
  220.     public Task NotFound(object? error = null)
  221.         => JSON(404, error ?? new { error = "Not Found" });
  222.     /// <summary>500 Internal Server Error</summary>
  223.     public Task InternalServerError(object? error = null)
  224.         => JSON(500, error ?? new { error = "Internal Server Error" });
  225.     /// <summary>中止并返回状态码</summary>
  226.     public Task AbortWithStatus(int statusCode)
  227.     {
  228.         Abort();
  229.         return Status(statusCode);
  230.     }
  231.     /// <summary>中止并返回 JSON 错误</summary>
  232.     public Task AbortWithJSON(int statusCode, object error)
  233.     {
  234.         Abort();
  235.         return JSON(statusCode, error);
  236.     }
  237.     #endregion
  238.     #region 私有方法
  239.     private bool TryStartResponse()
  240.     {
  241.         if (_responseSent) return false;
  242.         _responseSent = true;
  243.         return true;
  244.     }
  245.     private async Task WriteAndCloseAsync(byte[] bytes)
  246.     {
  247.         await Response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
  248.         Response.OutputStream.Close();
  249.     }
  250.     #endregion
  251. }
复制代码
2.Engine
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Text;
  6. using System.Text.Json;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. namespace MiniGin;
  10. /// <summary>
  11. /// Gin 风格的 HTTP 引擎 - 核心入口
  12. /// </summary>
  13. public class Engine : RouterGroup
  14. {
  15.     private readonly List<Route> _routes = new();
  16.     private readonly JsonSerializerOptions _jsonOptions;
  17.     private HttpListener? _listener;
  18.     private bool _swaggerEnabled;
  19.     private string _swaggerTitle = "MiniGin API";
  20.     private string _swaggerVersion = "v1";
  21.     /// <summary>
  22.     /// 创建新的引擎实例
  23.     /// </summary>
  24.     public Engine() : this(new JsonSerializerOptions(JsonSerializerDefaults.Web))
  25.     {
  26.     }
  27.     /// <summary>
  28.     /// 创建新的引擎实例(自定义 JSON 选项)
  29.     /// </summary>
  30.     public Engine(JsonSerializerOptions jsonOptions) : base(null!, "")
  31.     {
  32.         _jsonOptions = jsonOptions;
  33.         SetEngine(this);
  34.     }
  35.     private void SetEngine(Engine engine)
  36.     {
  37.         var field = typeof(RouterGroup).GetField("_engine", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  38.         field?.SetValue(this, engine);
  39.     }
  40.     #region 配置
  41.     /// <summary>
  42.     /// 启用 Swagger UI 和 OpenAPI 文档
  43.     /// </summary>
  44.     /// <param name="title">API 标题</param>
  45.     /// <param name="version">API 版本</param>
  46.     public Engine UseSwagger(string title = "MiniGin API", string version = "v1")
  47.     {
  48.         _swaggerEnabled = true;
  49.         _swaggerTitle = title;
  50.         _swaggerVersion = version;
  51.         return this;
  52.     }
  53.     /// <summary>
  54.     /// 获取所有已注册的路由
  55.     /// </summary>
  56.     public IReadOnlyList<Route> Routes => _routes;
  57.     /// <summary>
  58.     /// JSON 序列化选项
  59.     /// </summary>
  60.     public JsonSerializerOptions JsonOptions => _jsonOptions;
  61.     #endregion
  62.     #region 路由注册(内部)
  63.     internal void AddRoute(string method, string path, HandlerFunc[] handlers)
  64.     {
  65.         if (string.IsNullOrWhiteSpace(method))
  66.             throw new ArgumentException("HTTP method is required.", nameof(method));
  67.         if (handlers == null || handlers.Length == 0)
  68.             throw new ArgumentException("At least one handler is required.", nameof(handlers));
  69.         var pattern = RoutePattern.Parse(path);
  70.         var route = new Route(method.ToUpperInvariant(), path, pattern, handlers);
  71.         _routes.Add(route);
  72.         _routes.Sort((a, b) => b.Pattern.LiteralCount.CompareTo(a.Pattern.LiteralCount));
  73.     }
  74.     #endregion
  75.     #region 运行
  76.     /// <summary>
  77.     /// 启动 HTTP 服务器
  78.     /// </summary>
  79.     /// <param name="address">监听地址,如 http://localhost:5000/</param>
  80.     public Task Run(string address = "http://localhost:5000/")
  81.         => Run(address, CancellationToken.None);
  82.     /// <summary>
  83.     /// 启动 HTTP 服务器(支持取消)
  84.     /// </summary>
  85.     /// <param name="address">监听地址</param>
  86.     /// <param name="cancellationToken">取消令牌</param>
  87.     public async Task Run(string address, CancellationToken cancellationToken)
  88.     {
  89.         if (!address.EndsWith("/"))
  90.             address += "/";
  91.         _listener = new HttpListener();
  92.         _listener.Prefixes.Add(address);
  93.         _listener.Start();
  94.         Console.WriteLine($"[MiniGin] Listening on {address}");
  95.         if (_swaggerEnabled)
  96.             Console.WriteLine($"[MiniGin] Swagger UI: {address}swagger");
  97.         try
  98.         {
  99.             while (!cancellationToken.IsCancellationRequested)
  100.             {
  101.                 try
  102.                 {
  103.                     var httpContext = await _listener.GetContextAsync();
  104.                     _ = Task.Run(() => HandleRequestAsync(httpContext), cancellationToken);
  105.                 }
  106.                 catch (Exception ex) when (!(ex is HttpListenerException))
  107.                 {
  108.                     Console.WriteLine($"[MiniGin] Error accepting connection: {ex.Message}");
  109.                 }
  110.             }
  111.         }
  112.         catch (HttpListenerException) when (cancellationToken.IsCancellationRequested)
  113.         {
  114.             // 正常关闭
  115.         }
  116.         catch (Exception ex)
  117.         {
  118.             Console.WriteLine($"[MiniGin] Fatal error: {ex.Message}");
  119.             Console.WriteLine(ex.StackTrace);
  120.             throw;
  121.         }
  122.         finally
  123.         {
  124.             _listener.Stop();
  125.             _listener.Close();
  126.         }
  127.     }
  128.     /// <summary>
  129.     /// 停止服务器
  130.     /// </summary>
  131.     public void Stop()
  132.     {
  133.         _listener?.Stop();
  134.     }
  135.     private async Task HandleRequestAsync(HttpListenerContext httpContext)
  136.     {
  137.         try
  138.         {
  139.             var path = httpContext.Request.Url?.AbsolutePath ?? "/";
  140.             var method = httpContext.Request.HttpMethod ?? "GET";
  141.             // 处理 Swagger
  142.             if (_swaggerEnabled && await TryHandleSwaggerAsync(httpContext, path))
  143.                 return;
  144.             // 查找路由
  145.             var (route, routeParams) = FindRoute(method, path);
  146.             if (route == null)
  147.             {
  148.                 await WriteNotFound(httpContext.Response);
  149.                 return;
  150.             }
  151.             // 创建上下文
  152.             var ctx = new Context(httpContext, routeParams, _jsonOptions);
  153.             // 执行处理器链
  154.             await ExecuteHandlers(ctx, route.Handlers);
  155.         }
  156.         catch (Exception ex)
  157.         {
  158.             await WriteError(httpContext.Response, ex);
  159.         }
  160.     }
  161.     private async Task ExecuteHandlers(Context ctx, HandlerFunc[] handlers)
  162.     {
  163.         foreach (var handler in handlers)
  164.         {
  165.             if (ctx.IsAborted)
  166.                 break;
  167.             await handler(ctx);
  168.         }
  169.     }
  170.     private (Route? route, Dictionary<string, string> routeParams) FindRoute(string method, string path)
  171.     {
  172.         foreach (var route in _routes)
  173.         {
  174.             if (!string.Equals(route.Method, method, StringComparison.OrdinalIgnoreCase))
  175.                 continue;
  176.             if (route.Pattern.TryMatch(path, out var routeParams))
  177.                 return (route, routeParams);
  178.         }
  179.         return (null, new Dictionary<string, string>());
  180.     }
  181.     #endregion
  182.     #region Swagger
  183.     private async Task<bool> TryHandleSwaggerAsync(HttpListenerContext context, string path)
  184.     {
  185.         if (path.Equals("/swagger", StringComparison.OrdinalIgnoreCase) ||
  186.             path.Equals("/swagger/", StringComparison.OrdinalIgnoreCase))
  187.         {
  188.             var html = GenerateSwaggerHtml();
  189.             await WriteResponse(context.Response, 200, "text/html; charset=utf-8", html);
  190.             return true;
  191.         }
  192.         if (path.Equals("/swagger/v1/swagger.json", StringComparison.OrdinalIgnoreCase))
  193.         {
  194.             var doc = GenerateOpenApiDoc();
  195.             var json = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
  196.             await WriteResponse(context.Response, 200, "application/json; charset=utf-8", json);
  197.             return true;
  198.         }
  199.         return false;
  200.     }
  201.     private object GenerateOpenApiDoc()
  202.     {
  203.         var paths = new Dictionary<string, object>();
  204.         foreach (var routeGroup in _routes.GroupBy(r => r.OpenApiPath))
  205.         {
  206.             var operations = new Dictionary<string, object>();
  207.             foreach (var route in routeGroup)
  208.             {
  209.                 operations[route.Method.ToLowerInvariant()] = new
  210.                 {
  211.                     operationId = $"{route.Method}_{route.Path.Replace("/", "_").Replace(":", "")}",
  212.                     parameters = route.PathParameters.Select(p => new
  213.                     {
  214.                         name = p,
  215.                         @in = "path",
  216.                         required = true,
  217.                         schema = new { type = "string" }
  218.                     }).ToArray(),
  219.                     responses = new Dictionary<string, object>
  220.                     {
  221.                         ["200"] = new { description = "OK" }
  222.                     }
  223.                 };
  224.             }
  225.             paths[routeGroup.Key] = operations;
  226.         }
  227.         return new
  228.         {
  229.             openapi = "3.0.1",
  230.             info = new { title = _swaggerTitle, version = _swaggerVersion },
  231.             paths
  232.         };
  233.     }
  234.     private static string GenerateSwaggerHtml() => @"<!doctype html>
  235. <html>
  236. <head>
  237.     <meta charset=""utf-8"" />
  238.     <meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
  239.     <title>Swagger UI</title>
  240.     <link rel=""stylesheet"" href=""https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"" />
  241. </head>
  242. <body>
  243.    
  244.    
  245.    
  246. </body>
  247. </html>";
  248.     #endregion
  249.     #region 响应辅助
  250.     private static async Task WriteResponse(HttpListenerResponse response, int statusCode, string contentType, string body)
  251.     {
  252.         var bytes = Encoding.UTF8.GetBytes(body);
  253.         response.StatusCode = statusCode;
  254.         response.ContentType = contentType;
  255.         response.ContentLength64 = bytes.Length;
  256.         await response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
  257.         response.OutputStream.Close();
  258.     }
  259.     private static Task WriteNotFound(HttpListenerResponse response)
  260.         => WriteResponse(response, 404, "application/json", "{"error":"Not Found"}");
  261.     private static Task WriteError(HttpListenerResponse response, Exception ex)
  262.         => WriteResponse(response, 500, "application/json", $"{{"error":"{ex.Message.Replace(""", "\\"")}"}}");
  263.     #endregion
  264. }
复制代码
3.Gin
  1. namespace MiniGin;
  2. /// <summary>
  3. /// MiniGin 工厂方法
  4. /// </summary>
  5. public static class Gin
  6. {
  7.     /// <summary>
  8.     /// 创建默认引擎(包含 Logger 和 Recovery 中间件)
  9.     /// </summary>
  10.     public static Engine Default()
  11.     {
  12.         var engine = new Engine();
  13.         engine.Use(Middleware.Logger(), Middleware.Recovery());
  14.         return engine;
  15.     }
  16.     /// <summary>
  17.     /// 创建空白引擎(不包含任何中间件)
  18.     /// </summary>
  19.     public static Engine New()
  20.     {
  21.         return new Engine();
  22.     }
  23. }
复制代码
4.Interface
  1. namespace MiniGin;
  2. /// <summary>
  3. /// MiniGin 工厂方法
  4. /// </summary>
  5. public static class Gin
  6. {
  7.     /// <summary>
  8.     /// 创建默认引擎(包含 Logger 和 Recovery 中间件)
  9.     /// </summary>
  10.     public static Engine Default()
  11.     {
  12.         var engine = new Engine();
  13.         engine.Use(Middleware.Logger(), Middleware.Recovery());
  14.         return engine;
  15.     }
  16.     /// <summary>
  17.     /// 创建空白引擎(不包含任何中间件)
  18.     /// </summary>
  19.     public static Engine New()
  20.     {
  21.         return new Engine();
  22.     }
  23. }
复制代码
5.Middleware
  1. using System;
  2. using System.Diagnostics;
  3. using System.Threading.Tasks;
  4. namespace MiniGin;
  5. /// <summary>
  6. /// 内置中间件集合
  7. /// </summary>
  8. public static class Middleware
  9. {
  10.     /// <summary>
  11.     /// 请求日志中间件
  12.     /// </summary>
  13.     /// <param name="logger">自定义日志输出(默认 Console.WriteLine)</param>
  14.     public static HandlerFunc Logger(Action<string>? logger = null)
  15.     {
  16.         logger ??= Console.WriteLine;
  17.         return ctx =>
  18.         {
  19.             var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  20.             logger($"[{timestamp}] {ctx.Method} {ctx.Path} from {ctx.ClientIP}");
  21.             return Task.CompletedTask;
  22.         };
  23.     }
  24.     /// <summary>
  25.     /// 请求计时中间件
  26.     /// </summary>
  27.     /// <param name="callback">计时回调</param>
  28.     public static HandlerFunc Timer(Action<Context, long>? callback = null)
  29.     {
  30.         callback ??= (ctx, ms) => Console.WriteLine($"[Timer] {ctx.Method} {ctx.Path} - {ms}ms");
  31.         return ctx =>
  32.         {
  33.             var sw = Stopwatch.StartNew();
  34.             ctx.Set("__timer_start", sw);
  35.             ctx.Set("__timer_callback", (Action)(() =>
  36.             {
  37.                 sw.Stop();
  38.                 callback(ctx, sw.ElapsedMilliseconds);
  39.             }));
  40.             return Task.CompletedTask;
  41.         };
  42.     }
  43.     /// <summary>
  44.     /// 错误恢复中间件
  45.     /// </summary>
  46.     /// <param name="showStackTrace">是否显示堆栈跟踪</param>
  47.     public static HandlerFunc Recovery(bool showStackTrace = false)
  48.     {
  49.         return async ctx =>
  50.         {
  51.             try
  52.             {
  53.                 // 预留用于自定义错误处理
  54.             }
  55.             catch (Exception ex)
  56.             {
  57.                 var message = showStackTrace ? ex.ToString() : ex.Message;
  58.                 await ctx.JSON(500, new
  59.                 {
  60.                     error = true,
  61.                     message,
  62.                     timestamp = DateTime.UtcNow
  63.                 });
  64.                 ctx.Abort();
  65.             }
  66.         };
  67.     }
  68.     /// <summary>
  69.     /// CORS 中间件
  70.     /// </summary>
  71.     /// <param name="config">CORS 配置</param>
  72.     public static HandlerFunc CORS(CorsConfig? config = null)
  73.     {
  74.         config ??= new CorsConfig();
  75.         return async ctx =>
  76.         {
  77.             ctx.Header("Access-Control-Allow-Origin", config.AllowOrigins)
  78.                .Header("Access-Control-Allow-Methods", config.AllowMethods)
  79.                .Header("Access-Control-Allow-Headers", config.AllowHeaders);
  80.             if (config.AllowCredentials)
  81.                 ctx.Header("Access-Control-Allow-Credentials", "true");
  82.             if (config.MaxAge > 0)
  83.                 ctx.Header("Access-Control-Max-Age", config.MaxAge.ToString());
  84.             // 预检请求直接返回
  85.             if (ctx.Method == "OPTIONS")
  86.             {
  87.                 await ctx.Status(204);
  88.                 ctx.Abort();
  89.             }
  90.         };
  91.     }
  92.     /// <summary>
  93.     /// HTTP Basic 认证中间件
  94.     /// </summary>
  95.     /// <param name="validator">用户名密码验证器</param>
  96.     /// <param name="realm">认证域</param>
  97.     public static HandlerFunc BasicAuth(Func<string, string, bool> validator, string realm = "Authorization Required")
  98.     {
  99.         return async ctx =>
  100.         {
  101.             var authHeader = ctx.GetHeader("Authorization");
  102.             if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
  103.             {
  104.                 ctx.Header("WWW-Authenticate", $"Basic realm="{realm}"");
  105.                 await ctx.Unauthorized(new { error = "Unauthorized" });
  106.                 ctx.Abort();
  107.                 return;
  108.             }
  109.             try
  110.             {
  111.                 var encoded = authHeader["Basic ".Length..];
  112.                 var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
  113.                 var parts = decoded.Split(':', 2);
  114.                 if (parts.Length != 2 || !validator(parts[0], parts[1]))
  115.                 {
  116.                     await ctx.Unauthorized(new { error = "Invalid credentials" });
  117.                     ctx.Abort();
  118.                 }
  119.                 else
  120.                 {
  121.                     ctx.Set("user", parts[0]);
  122.                 }
  123.             }
  124.             catch
  125.             {
  126.                 await ctx.Unauthorized(new { error = "Invalid authorization header" });
  127.                 ctx.Abort();
  128.             }
  129.         };
  130.     }
  131.     /// <summary>
  132.     /// API Key 认证中间件
  133.     /// </summary>
  134.     /// <param name="headerName">请求头名称</param>
  135.     /// <param name="validator">API Key 验证器</param>
  136.     public static HandlerFunc ApiKey(string headerName, Func<string?, bool> validator)
  137.     {
  138.         return async ctx =>
  139.         {
  140.             var apiKey = ctx.GetHeader(headerName);
  141.             if (!validator(apiKey))
  142.             {
  143.                 await ctx.Unauthorized(new { error = "Invalid API Key" });
  144.                 ctx.Abort();
  145.             }
  146.         };
  147.     }
  148.     /// <summary>
  149.     /// 请求 ID 中间件
  150.     /// </summary>
  151.     /// <param name="headerName">请求头名称</param>
  152.     public static HandlerFunc RequestId(string headerName = "X-Request-ID")
  153.     {
  154.         return ctx =>
  155.         {
  156.             var requestId = ctx.GetHeader(headerName);
  157.             if (string.IsNullOrEmpty(requestId))
  158.                 requestId = Guid.NewGuid().ToString("N");
  159.             ctx.Set("RequestId", requestId);
  160.             ctx.Header(headerName, requestId);
  161.             return Task.CompletedTask;
  162.         };
  163.     }
  164.     /// <summary>
  165.     /// 自定义响应头中间件
  166.     /// </summary>
  167.     /// <param name="headers">响应头键值对</param>
  168.     public static HandlerFunc Headers(params (string key, string value)[] headers)
  169.     {
  170.         return ctx =>
  171.         {
  172.             foreach (var (key, value) in headers)
  173.                 ctx.Header(key, value);
  174.             return Task.CompletedTask;
  175.         };
  176.     }
  177.     /// <summary>
  178.     /// 静态文件中间件(简单实现)
  179.     /// </summary>
  180.     /// <param name="urlPrefix">URL 前缀</param>
  181.     /// <param name="rootPath">文件系统根路径</param>
  182.     public static HandlerFunc Static(string urlPrefix, string rootPath)
  183.     {
  184.         return async ctx =>
  185.         {
  186.             if (!ctx.Path.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
  187.                 return;
  188.             var relativePath = ctx.Path[urlPrefix.Length..].TrimStart('/');
  189.             var filePath = System.IO.Path.Combine(rootPath, relativePath);
  190.             if (!System.IO.File.Exists(filePath))
  191.             {
  192.                 await ctx.NotFound();
  193.                 ctx.Abort();
  194.                 return;
  195.             }
  196.             var contentType = GetContentType(filePath);
  197.             var bytes = await System.IO.File.ReadAllBytesAsync(filePath);
  198.             await ctx.Data(200, contentType, bytes);
  199.             ctx.Abort();
  200.         };
  201.     }
  202.     private static string GetContentType(string filePath)
  203.     {
  204.         var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
  205.         return ext switch
  206.         {
  207.             ".html" or ".htm" => "text/html; charset=utf-8",
  208.             ".css" => "text/css; charset=utf-8",
  209.             ".js" => "application/javascript; charset=utf-8",
  210.             ".json" => "application/json; charset=utf-8",
  211.             ".png" => "image/png",
  212.             ".jpg" or ".jpeg" => "image/jpeg",
  213.             ".gif" => "image/gif",
  214.             ".svg" => "image/svg+xml",
  215.             ".ico" => "image/x-icon",
  216.             ".woff" => "font/woff",
  217.             ".woff2" => "font/woff2",
  218.             ".ttf" => "font/ttf",
  219.             ".pdf" => "application/pdf",
  220.             ".xml" => "application/xml",
  221.             _ => "application/octet-stream"
  222.         };
  223.     }
  224. }
  225. /// <summary>
  226. /// CORS 配置
  227. /// </summary>
  228. public class CorsConfig
  229. {
  230.     /// <summary>允许的源</summary>
  231.     public string AllowOrigins { get; set; } = "*";
  232.     /// <summary>允许的方法</summary>
  233.     public string AllowMethods { get; set; } = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
  234.     /// <summary>允许的请求头</summary>
  235.     public string AllowHeaders { get; set; } = "Content-Type, Authorization, X-Requested-With";
  236.     /// <summary>是否允许携带凭据</summary>
  237.     public bool AllowCredentials { get; set; } = false;
  238.     /// <summary>预检请求缓存时间(秒)</summary>
  239.     public int MaxAge { get; set; } = 86400;
  240. }
复制代码
6.Route
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. namespace MiniGin;
  5. /// <summary>
  6. /// 路由定义
  7. /// </summary>
  8. public sealed class Route
  9. {
  10.     /// <summary>
  11.     /// 创建路由定义
  12.     /// </summary>
  13.     public Route(string method, string path, RoutePattern pattern, HandlerFunc[] handlers)
  14.     {
  15.         Method = method;
  16.         Path = path;
  17.         Pattern = pattern;
  18.         Handlers = handlers;
  19.     }
  20.     /// <summary>HTTP 方法</summary>
  21.     public string Method { get; }
  22.     /// <summary>路由路径</summary>
  23.     public string Path { get; }
  24.     /// <summary>路由模式</summary>
  25.     public RoutePattern Pattern { get; }
  26.     /// <summary>处理器链</summary>
  27.     public HandlerFunc[] Handlers { get; }
  28.     /// <summary>OpenAPI 格式路径</summary>
  29.     public string OpenApiPath => Path.Split('/')
  30.         .Select(s => s.StartsWith(":") ? "{" + s[1..] + "}" : s)
  31.         .Aggregate((a, b) => a + "/" + b);
  32.     /// <summary>路径参数列表</summary>
  33.     public string[] PathParameters => Path.Split('/')
  34.         .Where(s => s.StartsWith(":"))
  35.         .Select(s => s[1..])
  36.         .ToArray();
  37. }
  38. /// <summary>
  39. /// 路由模式解析
  40. /// </summary>
  41. public sealed class RoutePattern
  42. {
  43.     private readonly Segment[] _segments;
  44.     private RoutePattern(Segment[] segments) => _segments = segments;
  45.     /// <summary>
  46.     /// 解析路由模式
  47.     /// </summary>
  48.     public static RoutePattern Parse(string path)
  49.     {
  50.         var cleaned = (path ?? "/").Trim().Trim('/');
  51.         if (string.IsNullOrEmpty(cleaned))
  52.             return new RoutePattern(Array.Empty<Segment>());
  53.         var parts = cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
  54.         var segments = parts.Select(ParseSegment).ToArray();
  55.         return new RoutePattern(segments);
  56.     }
  57.     private static Segment ParseSegment(string part)
  58.     {
  59.         if (part.StartsWith(":"))
  60.             return new Segment(true, part[1..], false);
  61.         if (part.StartsWith("*"))
  62.             return new Segment(true, part[1..], true);
  63.         return new Segment(false, part, false);
  64.     }
  65.     /// <summary>
  66.     /// 尝试匹配请求路径
  67.     /// </summary>
  68.     public bool TryMatch(string requestPath, out Dictionary<string, string> routeParams)
  69.     {
  70.         routeParams = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  71.         var cleaned = (requestPath ?? "/").Trim().Trim('/');
  72.         var parts = string.IsNullOrEmpty(cleaned)
  73.             ? Array.Empty<string>()
  74.             : cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
  75.         // 检查通配符
  76.         var hasWildcard = _segments.Any(s => s.IsWildcard);
  77.         if (!hasWildcard && parts.Length != _segments.Length)
  78.             return false;
  79.         for (var i = 0; i < _segments.Length; i++)
  80.         {
  81.             var segment = _segments[i];
  82.             if (segment.IsWildcard)
  83.             {
  84.                 // 通配符匹配剩余所有路径
  85.                 var remaining = string.Join("/", parts.Skip(i));
  86.                 routeParams[segment.Value] = Uri.UnescapeDataString(remaining);
  87.                 return true;
  88.             }
  89.             if (i >= parts.Length)
  90.                 return false;
  91.             var value = parts[i];
  92.             if (segment.IsParam)
  93.             {
  94.                 routeParams[segment.Value] = Uri.UnescapeDataString(value);
  95.             }
  96.             else if (!string.Equals(segment.Value, value, StringComparison.OrdinalIgnoreCase))
  97.             {
  98.                 return false;
  99.             }
  100.         }
  101.         return true;
  102.     }
  103.     /// <summary>字面量段数量(用于排序)</summary>
  104.     public int LiteralCount => _segments.Count(s => !s.IsParam);
  105.     private readonly record struct Segment(bool IsParam, string Value, bool IsWildcard = false);
  106. }
复制代码
7.RouteGroup
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. namespace MiniGin;
  5. /// <summary>
  6. /// 路由定义
  7. /// </summary>
  8. public sealed class Route
  9. {
  10.     /// <summary>
  11.     /// 创建路由定义
  12.     /// </summary>
  13.     public Route(string method, string path, RoutePattern pattern, HandlerFunc[] handlers)
  14.     {
  15.         Method = method;
  16.         Path = path;
  17.         Pattern = pattern;
  18.         Handlers = handlers;
  19.     }
  20.     /// <summary>HTTP 方法</summary>
  21.     public string Method { get; }
  22.     /// <summary>路由路径</summary>
  23.     public string Path { get; }
  24.     /// <summary>路由模式</summary>
  25.     public RoutePattern Pattern { get; }
  26.     /// <summary>处理器链</summary>
  27.     public HandlerFunc[] Handlers { get; }
  28.     /// <summary>OpenAPI 格式路径</summary>
  29.     public string OpenApiPath => Path.Split('/')
  30.         .Select(s => s.StartsWith(":") ? "{" + s[1..] + "}" : s)
  31.         .Aggregate((a, b) => a + "/" + b);
  32.     /// <summary>路径参数列表</summary>
  33.     public string[] PathParameters => Path.Split('/')
  34.         .Where(s => s.StartsWith(":"))
  35.         .Select(s => s[1..])
  36.         .ToArray();
  37. }
  38. /// <summary>
  39. /// 路由模式解析
  40. /// </summary>
  41. public sealed class RoutePattern
  42. {
  43.     private readonly Segment[] _segments;
  44.     private RoutePattern(Segment[] segments) => _segments = segments;
  45.     /// <summary>
  46.     /// 解析路由模式
  47.     /// </summary>
  48.     public static RoutePattern Parse(string path)
  49.     {
  50.         var cleaned = (path ?? "/").Trim().Trim('/');
  51.         if (string.IsNullOrEmpty(cleaned))
  52.             return new RoutePattern(Array.Empty<Segment>());
  53.         var parts = cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
  54.         var segments = parts.Select(ParseSegment).ToArray();
  55.         return new RoutePattern(segments);
  56.     }
  57.     private static Segment ParseSegment(string part)
  58.     {
  59.         if (part.StartsWith(":"))
  60.             return new Segment(true, part[1..], false);
  61.         if (part.StartsWith("*"))
  62.             return new Segment(true, part[1..], true);
  63.         return new Segment(false, part, false);
  64.     }
  65.     /// <summary>
  66.     /// 尝试匹配请求路径
  67.     /// </summary>
  68.     public bool TryMatch(string requestPath, out Dictionary<string, string> routeParams)
  69.     {
  70.         routeParams = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  71.         var cleaned = (requestPath ?? "/").Trim().Trim('/');
  72.         var parts = string.IsNullOrEmpty(cleaned)
  73.             ? Array.Empty<string>()
  74.             : cleaned.Split('/', StringSplitOptions.RemoveEmptyEntries);
  75.         // 检查通配符
  76.         var hasWildcard = _segments.Any(s => s.IsWildcard);
  77.         if (!hasWildcard && parts.Length != _segments.Length)
  78.             return false;
  79.         for (var i = 0; i < _segments.Length; i++)
  80.         {
  81.             var segment = _segments[i];
  82.             if (segment.IsWildcard)
  83.             {
  84.                 // 通配符匹配剩余所有路径
  85.                 var remaining = string.Join("/", parts.Skip(i));
  86.                 routeParams[segment.Value] = Uri.UnescapeDataString(remaining);
  87.                 return true;
  88.             }
  89.             if (i >= parts.Length)
  90.                 return false;
  91.             var value = parts[i];
  92.             if (segment.IsParam)
  93.             {
  94.                 routeParams[segment.Value] = Uri.UnescapeDataString(value);
  95.             }
  96.             else if (!string.Equals(segment.Value, value, StringComparison.OrdinalIgnoreCase))
  97.             {
  98.                 return false;
  99.             }
  100.         }
  101.         return true;
  102.     }
  103.     /// <summary>字面量段数量(用于排序)</summary>
  104.     public int LiteralCount => _segments.Count(s => !s.IsParam);
  105.     private readonly record struct Segment(bool IsParam, string Value, bool IsWildcard = false);
  106. }
复制代码
 
8.MiniHttpApi.csproj
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.   <PropertyGroup>
  3.     <OutputType>Exe</OutputType>
  4.     <TargetFramework>net8.0</TargetFramework>
  5.     <ImplicitUsings>enable</ImplicitUsings>
  6.     <Nullable>enable</Nullable>
  7.   </PropertyGroup>
  8.   
  9.   <ItemGroup>
  10.     <Compile Remove="MiniGin\**\*.cs" />
  11.     <None Remove="MiniGin\**\*" />
  12.   </ItemGroup>
  13.   <ItemGroup>
  14.     <ProjectReference Include="MiniGin\MiniGin.csproj" />
  15.   </ItemGroup>
  16. </Project>
复制代码
9.MiniGin.csproj
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.   <PropertyGroup>
  3.     <TargetFramework>net8.0</TargetFramework>
  4.     <ImplicitUsings>enable</ImplicitUsings>
  5.     <Nullable>enable</Nullable>
  6.    
  7.    
  8.     <PackageId>MiniGin</PackageId>
  9.     <Version>1.0.0</Version>
  10.     Your Name</Authors>
  11.     <Company>Your Company</Company>
  12.     <Description>A lightweight Gin-style HTTP framework for .NET based on HttpListener</Description>
  13.     <PackageTags>http;web;framework;gin;api;rest</PackageTags>
  14.     <PackageLicenseExpression>MIT</PackageLicenseExpression>
  15.     <PackageReadmeFile>README.md</PackageReadmeFile>
  16.    
  17.    
  18.     <GenerateDocumentationFile>true</GenerateDocumentationFile>
  19.     <NoWarn>$(NoWarn);CS1591</NoWarn>
  20.    
  21.    
  22.     <RootNamespace>MiniGin</RootNamespace>
  23.     MiniGin</AssemblyName>
  24.   </PropertyGroup>
  25.   <ItemGroup>
  26.     <None Include="README.md" Pack="true" PackagePath="" />
  27.   </ItemGroup>
  28. </Project>
复制代码
10.source
exercisebook/custom-webapi at main · liuzhixin405/exercisebook



来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册