Windows Web端功能 - C# 示例

Windows平台C# Web端功能示例,包括路由处理、中间件和静态文件服务

💻 HTTP请求路由 csharp

🟡 intermediate ⭐⭐⭐

实现灵活的HTTP请求路由,支持参数处理和路由约束

⏱️ 25 min 🏷️ csharp, routing, http, web, windows
Prerequisites: HTTP basics, Regular expressions, Async programming
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Net;
using System.IO;

// HTTP Request and Response classes
public class HttpRequest
{
    public string Method { get; set; }
    public string Path { get; set; }
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
    public string Body { get; set; }
    public Dictionary<string, string> QueryParameters { get; set; } = new Dictionary<string, string>();
    public Dictionary<string, string> PathParameters { get; set; } = new Dictionary<string, string>();
}

public class HttpResponse
{
    public int StatusCode { get; set; } = 200;
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
    public string Body { get; set; }

    public void SetJson(string json)
    {
        Headers["Content-Type"] = "application/json";
        Body = json;
    }

    public void SetHtml(string html)
    {
        Headers["Content-Type"] = "text/html";
        Body = html;
    }

    public void SetText(string text)
    {
        Headers["Content-Type"] = "text/plain";
        Body = text;
    }

    public void SetFile(string filePath, string contentType = null)
    {
        if (File.Exists(filePath))
        {
            var fileInfo = new FileInfo(filePath);
            Headers["Content-Type"] = contentType ?? GetContentType(fileInfo.Extension);
            Headers["Content-Length"] = fileInfo.Length.ToString();
            Body = File.ReadAllText(filePath);
        }
    }

    private string GetContentType(string extension)
    {
        return extension.ToLower() switch
        {
            ".html" or ".htm" => "text/html",
            ".css" => "text/css",
            ".js" => "application/javascript",
            ".json" => "application/json",
            ".png" => "image/png",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".gif" => "image/gif",
            ".svg" => "image/svg+xml",
            _ => "application/octet-stream"
        };
    }
}

// Route delegate type
public delegate Task<HttpResponse> RouteHandler(HttpRequest request);

// Route class
public class Route
{
    public string Method { get; set; }
    public string Pattern { get; set; }
    public RouteHandler Handler { get; set; }
    public Regex CompiledPattern { get; set; }

    public Route(string method, string pattern, RouteHandler handler)
    {
        Method = method.ToUpper();
        Pattern = pattern;
        Handler = handler;
        CompiledPattern = CompilePattern(pattern);
    }

    private Regex CompilePattern(string pattern)
    {
        // Convert route pattern to regex
        string regexPattern = "^" + Regex.Escape(pattern)
            .Replace(@"\*", ".*")
            .Replace(@":", @"(?<") + @"(?<name>[^/]+)" + @">" + @")" + @"";

        return new Regex(regexPattern);
    }

    public bool Matches(string method, string path, out Dictionary<string, string> parameters)
    {
        parameters = new Dictionary<string, string>();

        if (Method != "*" && Method != method.ToUpper())
            return false;

        Match match = CompiledPattern.Match(path);
        if (!match.Success)
            return false;

        // Extract parameter values
        foreach (Group group in match.Groups)
        {
            if (group.Name != "0" && !int.TryParse(group.Name, out _))
            {
                parameters[group.Name] = group.Value;
            }
        }

        return true;
    }
}

// Router class
public class Router
{
    private readonly List<Route> _routes = new List<Route>();

    // 1. Add route methods
    public void AddRoute(string method, string pattern, RouteHandler handler)
    {
        var route = new Route(method, pattern, handler);
        _routes.Add(route);
        Console.WriteLine($"Added route: {method} {pattern}");
    }

    public void Get(string pattern, RouteHandler handler)
    {
        AddRoute("GET", pattern, handler);
    }

    public void Post(string pattern, RouteHandler handler)
    {
        AddRoute("POST", pattern, handler);
    }

    public void Put(string pattern, RouteHandler handler)
    {
        AddRoute("PUT", pattern, handler);
    }

    public void Delete(string pattern, RouteHandler handler)
    {
        AddRoute("DELETE", pattern, handler);
    }

    public void Any(string pattern, RouteHandler handler)
    {
        AddRoute("*", pattern, handler);
    }

    // 2. Route matching
    public async Task<HttpResponse> RouteRequest(HttpRequest request)
    {
        foreach (var route in _routes)
        {
            if (route.Matches(request.Method, request.Path, out var parameters))
            {
                request.PathParameters = parameters;
                Console.WriteLine($"Route matched: {request.Method} {request.Pattern}");

                try
                {
                    return await route.Handler(request);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error handling route: {ex.Message}");
                    return CreateErrorResponse(500, "Internal Server Error");
                }
            }
        }

        return CreateErrorResponse(404, "Not Found");
    }

    private HttpResponse CreateErrorResponse(int statusCode, string message)
    {
        var response = new HttpResponse
        {
            StatusCode = statusCode,
            Body = $"{{"error": "{message}", "status": {statusCode}}}"
        };
        response.SetJson(response.Body);
        return response;
    }
}

// Routing examples
public class RoutingExamples
{
    private Router _router;

    public RoutingExamples()
    {
        _router = new Router();
        SetupRoutes();
    }

    // 3. Setup example routes
    private void SetupRoutes()
    {
        // Home page
        _router.Get("/", async (request) =>
        {
            var response = new HttpResponse();
            response.SetHtml(@"
                <html>
                    <head><title>Web Routing Examples</title></head>
                    <body>
                        <h1>Welcome to Web Routing Examples</h1>
                        <p>Try these routes:</p>
                        <ul>
                            <li><a href='/api/hello'>GET /api/hello</a></li>
                            <li><a href='/api/hello/John'>GET /api/hello/John</a></li>
                            <li>POST /api/users (with JSON body)</li>
                            <li><a href='/api/users/123'>GET /api/users/123</a></li>
                            <li><a href='/api/search?q=csharp'>GET /api/search?q=csharp</a></li>
                            <li><a href='/files/test.txt'>GET /files/test.txt</a></li>
                        </ul>
                    </body>
                </html>
            ");
            return response;
        });

        // Simple API endpoint
        _router.Get("/api/hello", async (request) =>
        {
            var response = new HttpResponse();
            var name = request.PathParameters.GetValueOrDefault("name", "World");
            var json = $"{{"message": "Hello, {name}!", "timestamp": "{DateTime.Now:yyyy-MM-dd HH:mm:ss}"}}";
            response.SetJson(json);
            return response;
        });

        // Parameterized route
        _router.Get("/api/hello/:name", async (request) =>
        {
            var response = new HttpResponse();
            var name = request.PathParameters.GetValueOrDefault("name", "Unknown");
            var json = $"{{"greeting": "Hello, {name}!", "method": "{request.Method}", "path": "{request.Path}"}}";
            response.SetJson(json);
            return response;
        });

        // User endpoints
        _router.Get("/api/users/:id", async (request) =>
        {
            var response = new HttpResponse();
            var userId = request.PathParameters.GetValueOrDefault("id", "0");
            var user = new
            {
                id = userId,
                name = $"User {userId}",
                email = $"user{userId}@example.com",
                created = DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd")
            };
            var json = System.Text.Json.JsonSerializer.Serialize(user);
            response.SetJson(json);
            return response;
        });

        _router.Post("/api/users", async (request) =>
        {
            var response = new HttpResponse();
            // In a real app, you'd parse and validate the request body
            var newUser = new
            {
                id = new Random().Next(1000, 9999).ToString(),
                status = "created",
                message = "User created successfully",
                timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
            };
            var json = System.Text.Json.JsonSerializer.Serialize(newUser);
            response.SetJson(json);
            return response;
        });

        // Search endpoint with query parameters
        _router.Get("/api/search", async (request) =>
        {
            var response = new HttpResponse();
            var query = request.QueryParameters.GetValueOrDefault("q", "");
            var limit = request.QueryParameters.GetValueOrDefault("limit", "10");

            var searchResult = new
            {
                query = query,
                results = new[] { $"Result 1 for {query}", $"Result 2 for {query}", $"Result 3 for {query}" },
                limit = int.Parse(limit),
                total = 3
            };
            var json = System.Text.Json.JsonSerializer.Serialize(searchResult);
            response.SetJson(json);
            return response;
        });

        // File serving route
        _router.Get("/files/*", async (request) =>
        {
            var response = new HttpResponse();
            var filename = request.PathParameters.GetValueOrDefault("path", "");
            var filePath = Path.Combine("wwwroot", filename);

            if (File.Exists(filePath))
            {
                response.SetFile(filePath);
            }
            else
            {
                response.StatusCode = 404;
                response.Body = "File not found";
            }
            return response;
        });

        // Wildcard route for API
        _router.Any("/api/*", async (request) =>
        {
            var response = new HttpResponse();
            response.StatusCode = 405; // Method Not Allowed
            var errorJson = $"{{"error": "Method not allowed", "path": "{request.Path}"}}";
            response.SetJson(errorJson);
            return response;
        });
    }

    // 4. Simulate HTTP requests for testing
    public async Task SimulateRequests()
    {
        Console.WriteLine("=== Simulating HTTP Requests ===\n");

        var requests = new[]
        {
            new HttpRequest { Method = "GET", Path = "/" },
            new HttpRequest { Method = "GET", Path = "/api/hello" },
            new HttpRequest { Method = "GET", Path = "/api/hello/John" },
            new HttpRequest { Method = "POST", Path = "/api/users", Body = "{\"name\": \"John Doe\", \"email\": \"[email protected]\"}" },
            new HttpRequest { Method = "GET", Path = "/api/users/123" },
            new HttpRequest { Method = "GET", Path = "/api/search", QueryParameters = new Dictionary<string, string> { { "q", "csharp" }, { "limit", "5" } } },
            new HttpRequest { Method = "GET", Path = "/api/nonexistent" },
            new HttpRequest { Method = "GET", Path = "/files/style.css" },
            new HttpRequest { Method = "PUT", Path = "/api/test" }
        };

        foreach (var request in requests)
        {
            Console.WriteLine($"Request: {request.Method} {request.Path}");

            if (request.QueryParameters.Count > 0)
            {
                var queryString = string.Join("&", request.QueryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
                Console.WriteLine($"  Query: {queryString}");
            }

            if (!string.IsNullOrEmpty(request.Body))
            {
                Console.WriteLine($"  Body: {request.Body}");
            }

            var response = await _router.RouteRequest(request);
            Console.WriteLine($"Response: {response.StatusCode} ({response.Body?.Length ?? 0} bytes)");

            if (response.Headers.ContainsKey("Content-Type"))
            {
                Console.WriteLine($"  Content-Type: {response.Headers["Content-Type"]}");
            }

            if (response.Body != null && response.Body.Length < 200)
            {
                Console.WriteLine($"  Body: {response.Body}");
            }

            Console.WriteLine();
        }
    }

    // 5. Advanced routing with route constraints
    public void AdvancedRoutingExamples()
    {
        Console.WriteLine("=== Advanced Routing Examples ===");

        var router = new Router();

        // Route with constraint (numeric ID)
        router.Get("/api/products/:id(\d+)", async (request) =>
        {
            var id = request.PathParameters["id"];
            Console.WriteLine($"Numeric product ID: {id}");
            var response = new HttpResponse();
            response.SetJson($"{{"product": {id}, "type": "numeric"}}");
            return response;
        });

        // Route with constraint (email)
        router.Get("/api/users/:email(.+@.+\..+)", async (request) =>
        {
            var email = request.PathParameters["email"];
            Console.WriteLine($"Email parameter: {email}");
            var response = new HttpResponse();
            response.SetJson($"{{"email": "{email}", "valid": true}}");
            return response;
        });

        // Optional parameter
        router.Get("/api/posts/:year/:month?", async (request) =>
        {
            var year = request.PathParameters["year"];
            var month = request.PathParameters.GetValueOrDefault("month", "all");
            Console.WriteLine($"Posts for year {year}, month {month}");
            var response = new HttpResponse();
            response.SetJson($"{{"year": "{year}", "month": "{month}"}}");
            return response;
        });

        Console.WriteLine("Advanced routes configured successfully");
    }
}

// Main demonstration
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== C# Web Routing Examples ===\n");

        try
        {
            var examples = new RoutingExamples();

            // Run basic routing examples
            await examples.SimulateRequests();

            // Show advanced routing
            examples.AdvancedRoutingExamples();

            Console.WriteLine("Web routing examples completed successfully!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

💻 请求处理中间件 csharp

🟡 intermediate ⭐⭐⭐

实现请求处理管道,包括日志记录、身份认证和响应修改

⏱️ 30 min 🏷️ csharp, middleware, pipeline, web, windows
Prerequisites: HTTP basics, Async programming, Pipeline patterns
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Text;
using System.Security.Cryptography;

// HTTP Context
public class HttpContext
{
    public HttpRequest Request { get; set; }
    public HttpResponse Response { get; set; }
    public IDictionary<string, object> Items { get; set; } = new Dictionary<string, object>();

    public HttpContext()
    {
        Request = new HttpRequest();
        Response = new HttpResponse();
    }
}

// Middleware delegate
public delegate Task<HttpContext> MiddlewareDelegate(HttpContext context, Func<HttpContext, Task<HttpContext>> next);

// Middleware pipeline builder
public class MiddlewarePipeline
{
    private readonly List<MiddlewareDelegate> _middlewares = new List<MiddlewareDelegate>();

    // 1. Add middleware to pipeline
    public MiddlewarePipeline Use(MiddlewareDelegate middleware)
    {
        _middlewares.Add(middleware);
        Console.WriteLine($"Added middleware: {middleware.Method.Name}");
        return this;
    }

    // 2. Build the pipeline
    public Func<HttpContext, Task<HttpContext>> Build()
    {
        Func<HttpContext, Task<HttpContext>> pipeline = context => Task.FromResult(context);

        // Build pipeline in reverse order
        for (int i = _middlewares.Count - 1; i >= 0; i--)
        {
            var middleware = _middlewares[i];
            var next = pipeline;
            pipeline = context => middleware(context, next);
        }

        return pipeline;
    }

    // 3. Execute pipeline
    public async Task<HttpContext> ExecuteAsync(HttpContext context)
    {
        var pipeline = Build();
        return await pipeline(context);
    }
}

// Middleware implementations
public static class Middlewares
{
    // 4. Request logging middleware
    public static async Task<HttpContext> RequestLogging(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        var stopwatch = Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString("N")[..8];

        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{requestId}] " +
                         $"{context.Request.Method} {context.Request.Path} - Started");

        try
        {
            // Add request ID to context
            context.Items["RequestId"] = requestId;
            context.Items["StartTime"] = DateTime.Now;

            // Call next middleware
            context = await next(context);

            stopwatch.Stop();
            var duration = stopwatch.ElapsedMilliseconds;

            Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{requestId}] " +
                             $"{context.Request.Method} {context.Request.Path} - " +
                             $"{context.Response.StatusCode} ({duration}ms)");

            return context;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{requestId}] " +
                             $"{context.Request.Method} {context.Request.Path} - " +
                             $"ERROR: {ex.Message} ({stopwatch.ElapsedMilliseconds}ms)");
            throw;
        }
    }

    // 5. Authentication middleware
    public static async Task<HttpContext> Authentication(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        Console.WriteLine($"[Auth] Checking authentication for {context.Request.Method} {context.Request.Path}");

        // Check for Authorization header
        if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
        {
            Console.WriteLine($"[Auth] Authorization header found: {authHeader}");

            // Simple token validation (in production, use proper JWT validation)
            if (IsValidToken(authHeader))
            {
                context.Items["User"] = new { Id = "123", Name = "John Doe", Role = "User" };
                context.Items["Authenticated"] = true;
                Console.WriteLine("[Auth] Authentication successful");
            }
            else
            {
                context.Response.StatusCode = 401;
                context.Response.SetJson(@"{""error"": ""Invalid or expired token""}");
                Console.WriteLine("[Auth] Authentication failed - Invalid token");
                return context;
            }
        }
        else
        {
            // Allow anonymous access for some routes
            if (IsPublicRoute(context.Request.Path))
            {
                context.Items["Authenticated"] = false;
                Console.WriteLine("[Auth] Public route - No authentication required");
            }
            else
            {
                context.Response.StatusCode = 401;
                context.Response.SetJson(@"{""error"": ""Authentication required""}");
                Console.WriteLine("[Auth] Authentication required");
                return context;
            }
        }

        return await next(context);
    }

    // 6. CORS middleware
    public static async Task<HttpContext> Cors(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        Console.WriteLine($"[CORS] Processing CORS for {context.Request.Method} {context.Request.Path}");

        // Add CORS headers
        context.Response.Headers["Access-Control-Allow-Origin"] = "*";
        context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS";
        context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization";

        // Handle preflight requests
        if (context.Request.Method == "OPTIONS")
        {
            context.Response.StatusCode = 200;
            Console.WriteLine("[CORS] Preflight request handled");
            return context;
        }

        return await next(context);
    }

    // 7. Rate limiting middleware
    public static async Task<HttpContext> RateLimiting(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        var clientId = GetClientId(context);
        Console.WriteLine($"[RateLimit] Processing request for client: {clientId}");

        // Simple rate limiting (in production, use a proper cache like Redis)
        if (IsRateLimited(clientId))
        {
            context.Response.StatusCode = 429;
            context.Response.SetJson(@"{""error"": ""Rate limit exceeded""}");
            context.Response.Headers["Retry-After"] = "60";
            Console.WriteLine($"[RateLimit] Client {clientId} rate limited");
            return context;
        }

        // Record request for rate limiting
        RecordRequest(clientId);

        return await next(context);
    }

    // 8. Request body parsing middleware
    public static async Task<HttpContext> RequestBodyParsing(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        if (!string.IsNullOrEmpty(context.Request.Body))
        {
            Console.WriteLine($"[BodyParser] Parsing request body ({context.Request.Body.Length} characters)");

            // Try to parse as JSON
            if (context.Request.Headers.TryGetValue("Content-Type", out var contentType) &&
                contentType.Contains("application/json"))
            {
                try
                {
                    // Simple JSON parsing (in production, use proper JSON library)
                    var parsedBody = ParseJsonSimple(context.Request.Body);
                    context.Items["ParsedBody"] = parsedBody;
                    Console.WriteLine($"[BodyParser] JSON parsed successfully");
                }
                catch (Exception ex)
                {
                    context.Response.StatusCode = 400;
                    context.Response.SetJson(@"{""error"": ""Invalid JSON""}");
                    Console.WriteLine($"[BodyParser] JSON parsing failed: {ex.Message}");
                    return context;
                }
            }
        }

        return await next(context);
    }

    // 9. Response compression middleware
    public static async Task<HttpContext> ResponseCompression(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        context = await next(context);

        if (context.Response.Body != null && context.Response.Body.Length > 1024)
        {
            Console.WriteLine($"[Compression] Compressing response ({context.Response.Body.Length} bytes)");

            // Simple compression simulation
            var originalSize = context.Response.Body.Length;
            var compressedBody = CompressString(context.Response.Body);

            context.Response.Body = compressedBody;
            context.Response.Headers["Content-Encoding"] = "gzip";

            var compressionRatio = (1 - (double)compressedBody.Length / originalSize) * 100;
            Console.WriteLine($"[Compression] Compressed from {originalSize} to {compressedBody.Length} bytes ({compressionRatio:F1}% reduction)");
        }

        return context;
    }

    // 10. Error handling middleware
    public static async Task<HttpContext> ErrorHandling(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        try
        {
            return await next(context);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[Error] Unhandled exception: {ex.Message}");
            Console.WriteLine($"[Error] Stack trace: {ex.StackTrace}");

            context.Response.StatusCode = 500;
            context.Response.SetJson($@"{{
                ""error"": ""Internal server error"",
                ""message"": ""An unexpected error occurred"",
                ""requestId"": ""{context.Items.GetValueOrDefault(""RequestId"", ""unknown"")}""
            }}");

            return context;
        }
    }

    // 11. Final handler middleware
    public static async Task<HttpContext> FinalHandler(HttpContext context, Func<HttpContext, Task<HttpContext>> next)
    {
        Console.WriteLine($"[Handler] Final handler for {context.Request.Method} {context.Request.Path}");

        // Generate response based on request
        if (context.Response.StatusCode == 0)
        {
            context.Response.StatusCode = 200;

            var response = new
            {
                message = "Request processed successfully",
                method = context.Request.Method,
                path = context.Request.Path,
                timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                authenticated = context.Items.GetValueOrDefault("Authenticated", false),
                requestId = context.Items.GetValueOrDefault("RequestId", "")
            };

            context.Response.SetJson(System.Text.Json.JsonSerializer.Serialize(response));
        }

        return await next(context);
    }

    // Helper methods
    private static bool IsValidToken(string token)
    {
        // Simple validation - check if token starts with "Bearer "
        return token.StartsWith("Bearer ") && token.Length > 20;
    }

    private static bool IsPublicRoute(string path)
    {
        var publicRoutes = new[] { "/", "/health", "/api/public" };
        return Array.Exists(publicRoutes, route => path == route || path.StartsWith(route));
    }

    private static string GetClientId(HttpContext context)
    {
        // Use IP address or User-Agent as client identifier
        return context.Request.Headers.GetValueOrDefault("X-Forwarded-For", "unknown");
    }

    private static bool IsRateLimited(string clientId)
    {
        // Simple rate limiting logic (in production, use proper time windows)
        return false; // Always allow for this example
    }

    private static void RecordRequest(string clientId)
    {
        // Record request for rate limiting (in production, store in cache/database)
        Console.WriteLine($"[RateLimit] Recorded request for client: {clientId}");
    }

    private static object ParseJsonSimple(string json)
    {
        // Simple JSON parsing placeholder
        return new { parsed = true, length = json.Length };
    }

    private static string CompressString(string input)
    {
        // Simulate compression by removing whitespace
        return string.Join("", input.Split(' ', StringSplitOptions.RemoveEmptyEntries));
    }
}

// Middleware pipeline examples
public class MiddlewareExamples
{
    // 12. Create different pipeline configurations
    public static async Task RunPipelineExamples()
    {
        Console.WriteLine("=== Middleware Pipeline Examples ===\n");

        // Example 1: Basic pipeline
        Console.WriteLine("1. Basic Pipeline:");
        await CreateBasicPipeline().ExecuteAsync(CreateSampleRequest("/"));

        Console.WriteLine("\n2. Authentication Pipeline:");
        await CreateAuthenticatedPipeline().ExecuteAsync(CreateAuthenticatedRequest("/api/protected"));

        Console.WriteLine("\n3. Full-featured Pipeline:");
        await CreateFullFeaturedPipeline().ExecuteAsync(CreateFullFeaturedRequest());

        Console.WriteLine("\n4. Error Handling Pipeline:");
        await CreateErrorHandlingPipeline().ExecuteAsync(CreateErrorRequest());
    }

    private static MiddlewarePipeline CreateBasicPipeline()
    {
        return new MiddlewarePipeline()
            .Use(Middlewares.RequestLogging)
            .Use(Middlewares.Cors)
            .Use(Middlewares.FinalHandler);
    }

    private static MiddlewarePipeline CreateAuthenticatedPipeline()
    {
        return new MiddlewarePipeline()
            .Use(Middlewares.RequestLogging)
            .Use(Middlewares.Authentication)
            .Use(Middlewares.FinalHandler);
    }

    private static MiddlewarePipeline CreateFullFeaturedPipeline()
    {
        return new MiddlewarePipeline()
            .Use(Middlewares.ErrorHandling)
            .Use(Middlewares.RequestLogging)
            .Use(Middlewares.Cors)
            .Use(Middlewares.RateLimiting)
            .Use(Middlewares.Authentication)
            .Use(Middlewares.RequestBodyParsing)
            .Use(Middlewares.ResponseCompression)
            .Use(Middlewares.FinalHandler);
    }

    private static MiddlewarePipeline CreateErrorHandlingPipeline()
    {
        return new MiddlewarePipeline()
            .Use(Middlewares.ErrorHandling)
            .Use(Middlewares.RequestLogging)
            .Use((context, next) => throw new InvalidOperationException("This is a test exception"));
    }

    private static HttpContext CreateSampleRequest(string path)
    {
        return new HttpContext
        {
            Request = new HttpRequest
            {
                Method = "GET",
                Path = path,
                Headers = new Dictionary<string, string>
                {
                    ["User-Agent"] = "Test Client"
                }
            }
        };
    }

    private static HttpContext CreateAuthenticatedRequest(string path)
    {
        return new HttpContext
        {
            Request = new HttpRequest
            {
                Method = "GET",
                Path = path,
                Headers = new Dictionary<string, string>
                {
                    ["Authorization"] = "Bearer valid-jwt-token-here",
                    ["Content-Type"] = "application/json"
                }
            }
        };
    }

    private static HttpContext CreateFullFeaturedRequest(string path = "/api/data")
    {
        return new HttpContext
        {
            Request = new HttpRequest
            {
                Method = "POST",
                Path = path,
                Headers = new Dictionary<string, string>
                {
                    ["Authorization"] = "Bearer valid-jwt-token-here",
                    ["Content-Type"] = "application/json",
                    ["User-Agent"] = "Full-Featured Test Client"
                },
                Body = @"{""name"": ""Test"", ""value"": 123}"
            }
        };
    }

    private static HttpContext CreateErrorRequest()
    {
        return new HttpContext
        {
            Request = new HttpRequest
            {
                Method = "GET",
                Path = "/api/error",
                Headers = new Dictionary<string, string>
                {
                    ["User-Agent"] = "Error Test Client"
                }
            }
        };
    }
}

// Main program
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== C# Middleware Examples ===\n");

        try
        {
            await MiddlewareExamples.RunPipelineExamples();

            Console.WriteLine("\nMiddleware examples completed successfully!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

💻 静态文件服务 csharp

🟡 intermediate ⭐⭐⭐

实现高效的静态文件服务,支持MIME类型检测、缓存头和安全特性

⏱️ 25 min 🏷️ csharp, static files, web server, windows
Prerequisites: File system operations, HTTP basics, Security concepts
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Text;
using System.Security.Cryptography;
using System.Linq;

// Static file configuration
public class StaticFileOptions
{
    public string RootDirectory { get; set; } = "wwwroot";
    public bool EnableDirectoryBrowsing { get; set; } = false;
    public bool EnableDefaultFiles { get; set; } = true;
    public string[] DefaultFileNames { get; set; } = { "index.html", "default.html", "home.html" };
    public bool EnableETag { get; set; } = true;
    public bool EnableLastModified { get; set; } = true;
    public int CacheMaxAge { get; set; } = 3600; // 1 hour
    public bool EnableCompression { get; set; } = true;
    public bool EnableRangeRequests { get; set; } = true;
    public string[] AllowedExtensions { get; set; } =
    {
        ".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".md",
        ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".ico",
        ".pdf", ".zip", ".tar", ".gz", ".woff", ".woff2", ".ttf", ".eot"
    };
}

// Static file server
public class StaticFileServer
{
    private readonly StaticFileOptions _options;

    public StaticFileServer(StaticFileOptions options = null)
    {
        _options = options ?? new StaticFileOptions();

        // Ensure root directory exists
        if (!Directory.Exists(_options.RootDirectory))
        {
            Directory.CreateDirectory(_options.RootDirectory);
            Console.WriteLine($"Created root directory: {_options.RootDirectory}");
        }

        CreateSampleFiles();
    }

    // 1. Serve static file
    public async Task<HttpResponse> ServeFileAsync(string relativePath)
    {
        Console.WriteLine($"[StaticFile] Request for: {relativePath}");

        try
        {
            // Normalize path
            relativePath = NormalizePath(relativePath);

            // Security: Check for path traversal attempts
            if (IsPathTraversal(relativePath))
            {
                Console.WriteLine("[StaticFile] Path traversal attempt detected");
                return CreateErrorResponse(403, "Forbidden");
            }

            // Resolve full path
            string fullPath = Path.Combine(_options.RootDirectory, relativePath);

            // Check if path exists
            if (!File.Exists(fullPath))
            {
                // Try to serve default file if it's a directory
                if (Directory.Exists(fullPath))
                {
                    if (_options.EnableDefaultFiles)
                    {
                        foreach (string defaultFile in _options.DefaultFileNames)
                        {
                            string defaultPath = Path.Combine(fullPath, defaultFile);
                            if (File.Exists(defaultPath))
                            {
                                return await ServeFileAsync(Path.Combine(relativePath, defaultFile));
                            }
                        }
                    }

                    // Directory browsing
                    if (_options.EnableDirectoryBrowsing)
                    {
                        return await ServeDirectoryListingAsync(relativePath, fullPath);
                    }
                }

                Console.WriteLine($"[StaticFile] File not found: {fullPath}");
                return CreateErrorResponse(404, "File Not Found");
            }

            // Check file extension
            string extension = Path.GetExtension(fullPath).ToLowerInvariant();
            if (!_options.AllowedExtensions.Contains(extension))
            {
                Console.WriteLine($"[StaticFile] File extension not allowed: {extension}");
                return CreateErrorResponse(403, "File type not allowed");
            }

            // Get file info
            var fileInfo = new FileInfo(fullPath);
            var response = new HttpResponse();

            // Set content type
            string contentType = GetContentType(extension);
            response.Headers["Content-Type"] = contentType;

            // Set caching headers
            if (_options.EnableETag)
            {
                string eTag = GenerateETag(fileInfo);
                response.Headers["ETag"] = eTag;
                response.Headers["Cache-Control"] = $"public, max-age={_options.CacheMaxAge}";
            }

            if (_options.EnableLastModified)
            {
                response.Headers["Last-Modified"] = fileInfo.LastWriteTime.ToString("R");
            }

            // Set file size
            response.Headers["Content-Length"] = fileInfo.Length.ToString();

            // Read file content
            if (IsTextFile(extension))
            {
                response.Body = await File.ReadAllTextAsync(fullPath);
            }
            else
            {
                // For binary files, in a real server you'd stream the content
                response.Body = Convert.ToBase64String(await File.ReadAllBytesAsync(fullPath));
                response.Headers["Content-Transfer-Encoding"] = "base64";
            }

            Console.WriteLine($"[StaticFile] Served: {relativePath} ({fileInfo.Length:N0} bytes, {contentType})");
            return response;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[StaticFile] Error serving file {relativePath}: {ex.Message}");
            return CreateErrorResponse(500, "Internal Server Error");
        }
    }

    // 2. Serve directory listing
    private async Task<HttpResponse> ServeDirectoryListingAsync(string relativePath, string fullPath)
    {
        Console.WriteLine($"[StaticFile] Serving directory listing for: {relativePath}");

        var response = new HttpResponse();
        response.Headers["Content-Type"] = "text/html";

        var directories = Directory.GetDirectories(fullPath)
            .Select(Path.GetFileName)
            .OrderBy(name => name)
            .ToList();

        var files = Directory.GetFiles(fullPath)
            .Select(Path.GetFileName)
            .Where(name => _options.AllowedExtensions.Contains(Path.GetExtension(name).ToLowerInvariant()))
            .OrderBy(name => name)
            .ToList();

        var html = new StringBuilder();
        html.AppendLine("<!DOCTYPE html>");
        html.AppendLine("<html>");
        html.AppendLine("<head>");
        html.AppendLine("    <title>Directory Listing</title>");
        html.AppendLine("    <style>");
        html.AppendLine("        body { font-family: Arial, sans-serif; margin: 20px; }");
        html.AppendLine("        h1 { color: #333; }");
        html.AppendLine("        ul { list-style-type: none; padding: 0; }");
        html.AppendLine("        li { margin: 5px 0; }");
        html.AppendLine("        a { text-decoration: none; color: #0066cc; }");
        html.AppendLine("        a:hover { text-decoration: underline; }");
        html.AppendLine("        .icon { margin-right: 10px; }");
        html.AppendLine("        .dir { font-weight: bold; }");
        html.AppendLine("        .size { color: #666; font-size: 0.9em; margin-left: 10px; }");
        html.AppendLine("    </style>");
        html.AppendLine("</head>");
        html.AppendLine("<body>");
        html.AppendLine($"    <h1>Directory listing for {relativePath}</h1>");
        html.AppendLine("    <ul>");

        // Parent directory link
        if (!string.IsNullOrEmpty(relativePath) && relativePath != "/")
        {
            string parentPath = Path.GetDirectoryName(relativePath).Replace("\\", "/");
            if (string.IsNullOrEmpty(parentPath)) parentPath = "/";
            html.AppendLine($"        <li><span class=\"icon\">📁</span><a href=\"{parentPath}\">../</a></li>");
        }

        // Directories
        foreach (string dir in directories)
        {
            string dirPath = Path.Combine(relativePath, dir).Replace("\\", "/");
            html.AppendLine($"        <li class=\"dir\"><span class=\"icon\">📁</span><a href=\"{dirPath}/\">{dir}/</a></li>");
        }

        // Files
        foreach (string file in files)
        {
            string filePath = Path.Combine(relativePath, file).Replace("\\", "/");
            string fullPathFile = Path.Combine(fullPath, file);
            var fileInfo = new FileInfo(fullPathFile);
            string icon = GetFileIcon(file);
            string size = FormatFileSize(fileInfo.Length);

            html.AppendLine($"        <li><span class=\"icon\">{icon}</span><a href=\"{filePath}\">{file}</a><span class=\"size\">{size}</span></li>");
        }

        html.AppendLine("    </ul>");
        html.AppendLine("    <hr>");
        html.AppendLine($"    <p>Total: {directories.Count} directories, {files.Count} files</p>");
        html.AppendLine("</body>");
        html.AppendLine("</html>");

        response.Body = html.ToString();
        return response;
    }

    // 3. MIME type detection
    private static string GetContentType(string extension)
    {
        return extension.ToLowerInvariant() switch
        {
            ".html" or ".htm" => "text/html",
            ".css" => "text/css",
            ".js" => "application/javascript",
            ".json" => "application/json",
            ".xml" => "application/xml",
            ".txt" => "text/plain",
            ".md" => "text/markdown",
            ".csv" => "text/csv",
            ".png" => "image/png",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".gif" => "image/gif",
            ".bmp" => "image/bmp",
            ".svg" => "image/svg+xml",
            ".ico" => "image/x-icon",
            ".pdf" => "application/pdf",
            ".zip" => "application/zip",
            ".tar" => "application/x-tar",
            ".gz" => "application/gzip",
            ".woff" => "font/woff",
            ".woff2" => "font/woff2",
            ".ttf" => "font/ttf",
            ".eot" => "application/vnd.ms-fontobject",
            _ => "application/octet-stream"
        };
    }

    // 4. File icon selection
    private static string GetFileIcon(string fileName)
    {
        string extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".html" or ".htm" => "🌐",
            ".css" => "🎨",
            ".js" => "⚡",
            ".json" => "📋",
            ".xml" => "📄",
            ".txt" or ".md" => "📝",
            ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => "🖼️",
            ".pdf" => "📕",
            ".zip" or ".tar" or ".gz" => "📦",
            ".woff" or ".woff2" or ".ttf" or ".eot" => "🔤",
            _ => "📄"
        };
    }

    // 5. ETag generation
    private static string GenerateETag(FileInfo fileInfo)
    {
        string hash = $"{fileInfo.LastWriteTime.Ticks}-{fileInfo.Length}";
        using (var sha256 = SHA256.Create())
        {
            byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(hash));
            string eTag = Convert.ToBase64String(hashBytes).Replace("+", "-").Replace("/", "_").TrimEnd('=');
            return $"W/\"{eTag}\"";
        }
    }

    // 6. Path normalization and security
    private static string NormalizePath(string path)
    {
        if (string.IsNullOrEmpty(path))
            return "/";

        path = path.Replace('\', '/');
        path = path.Trim('/');

        if (string.IsNullOrEmpty(path))
            return "/";

        return "/" + path;
    }

    private static bool IsPathTraversal(string path)
    {
        return path.Contains("..") || path.Contains("\\") || path.StartsWith("/");
    }

    // 7. File type detection
    private static bool IsTextFile(string extension)
    {
        string[] textExtensions =
        {
            ".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".md",
            ".csv", ".log", ".ini", ".config", ".yml", ".yaml"
        };
        return textExtensions.Contains(extension);
    }

    // 8. File size formatting
    private static string FormatFileSize(long bytes)
    {
        string[] sizes = { "B", "KB", "MB", "GB", "TB" };
        int order = 0;
        double size = bytes;

        while (size >= 1024 && order < sizes.Length - 1)
        {
            order++;
            size /= 1024;
        }

        return $"{size:F1} {sizes[order]}";
    }

    // 9. Create sample files for demonstration
    private void CreateSampleFiles()
    {
        Console.WriteLine("[StaticFile] Creating sample files...");

        try
        {
            // Create directory structure
            string cssDir = Path.Combine(_options.RootDirectory, "css");
            string jsDir = Path.Combine(_options.RootDirectory, "js");
            string imagesDir = Path.Combine(_options.RootDirectory, "images");

            Directory.CreateDirectory(cssDir);
            Directory.CreateDirectory(jsDir);
            Directory.CreateDirectory(imagesDir);

            // Create HTML file
            string indexPath = Path.Combine(_options.RootDirectory, "index.html");
            File.WriteAllText(indexPath, @"<!DOCTYPE html>
<html>
<head>
    <title>Static File Server Demo</title>
    <link rel='stylesheet' href='/css/style.css'>
</head>
<body>
    <h1>Welcome to Static File Server!</h1>
    <p>This is a demonstration of static file serving capabilities.</p>
    <img src='/images/demo.png' alt='Demo Image'>
    <script src='/js/app.js'></script>
</body>
</html>");

            // Create CSS file
            string cssPath = Path.Combine(cssDir, "style.css");
            File.WriteAllText(cssPath, @"body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 20px;
    background-color: #f5f5f5;
}

h1 {
    color: #333;
    text-align: center;
}

img {
    max-width: 200px;
    display: block;
    margin: 20px auto;
}");

            // Create JavaScript file
            string jsPath = Path.Combine(jsDir, "app.js");
            File.WriteAllText(jsPath, @"console.log('Static file server demo loaded!');
document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM loaded successfully!');
});");

            // Create a simple text file
            string textPath = Path.Combine(_options.RootDirectory, "README.txt");
            File.WriteAllText(textPath, @"Static File Server Demo
=====================

This directory contains sample files served by the static file server.

Features:
- MIME type detection
- Directory browsing (when enabled)
- Default file serving
- Security checks
- Caching headers
- ETag support");

            // Create JSON file
            string jsonPath = Path.Combine(_options.RootDirectory, "data.json");
            File.WriteAllText(jsonPath, @"{
    ""name"": ""Static File Server"",
    ""version"": ""1.0.0"",
    ""features"": [
        ""MIME type detection"",
        ""Directory browsing"",
        ""Default file serving"",
        ""Security checks""
    ],
    ""created"": """ + DateTime.Now.ToString("yyyy-MM-dd") + @"""
}");

            Console.WriteLine("[StaticFile] Sample files created successfully");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[StaticFile] Error creating sample files: {ex.Message}");
        }
    }

    // 10. Error response creation
    private static HttpResponse CreateErrorResponse(int statusCode, string message)
    {
        var response = new HttpResponse
        {
            StatusCode = statusCode,
            Headers = { ["Content-Type"] = "application/json" },
            Body = $@"{{
                ""error"": ""{message}"",
                ""status"": {statusCode},
                ""timestamp"": ""{DateTime.Now:yyyy-MM-dd HH:mm:ss}""
            }}"
        };
        return response;
    }

    // 11. Cleanup method
    public void Cleanup()
    {
        try
        {
            if (Directory.Exists(_options.RootDirectory))
            {
                Directory.Delete(_options.RootDirectory, true);
                Console.WriteLine($"[StaticFile] Cleaned up root directory: {_options.RootDirectory}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[StaticFile] Error during cleanup: {ex.Message}");
        }
    }
}

// HTTP Response class (simplified)
public class HttpResponse
{
    public int StatusCode { get; set; } = 200;
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
    public string Body { get; set; }
}

// Demonstration class
public class StaticFileExamples
{
    public static async Task RunExamples()
    {
        Console.WriteLine("=== C# Static File Serving Examples ===\n");

        var options = new StaticFileOptions
        {
            RootDirectory = "static_files_demo",
            EnableDirectoryBrowsing = true,
            EnableDefaultFiles = true,
            CacheMaxAge = 3600
        };

        var server = new StaticFileServer(options);

        // Test various file requests
        var testPaths = new[]
        {
            "/",
            "/index.html",
            "/css/style.css",
            "/js/app.js",
            "/README.txt",
            "/data.json",
            "/nonexistent.html",
            "/../system32/config.sys", // Path traversal test
            "/images/nonexistent.jpg",
            "/" // Directory listing
        };

        foreach (string path in testPaths)
        {
            Console.WriteLine($"\nTesting: {path}");
            var response = await server.ServeFileAsync(path);

            Console.WriteLine($"Status: {response.StatusCode}");
            if (response.Headers.ContainsKey("Content-Type"))
            {
                Console.WriteLine($"Content-Type: {response.Headers["Content-Type"]}");
            }

            if (response.Body != null && response.Body.Length < 500)
            {
                Console.WriteLine($"Body preview: {response.Body.Substring(0, Math.Min(100, response.Body.Length))}...");
            }
            else if (response.Body != null)
            {
                Console.WriteLine($"Body length: {response.Body:N0} characters");
            }
        }

        // Cleanup
        server.Cleanup();
        Console.WriteLine("\nStatic file serving examples completed!");
    }
}

// Main program
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== C# Static File Server Examples ===\n");

        try
        {
            await StaticFileExamples.RunExamples();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}