netgin框架,仅供学习源gin

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>

10.source
exercisebook/custom-webapi at main · liuzhixin405/exercisebook

posted @ 2025-12-15 22:55  星仔007  阅读(1)  评论(0)    收藏  举报