moklgy's blog moklgy's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 后端文章

    • 技术题
  • .netcore

    • 《asp.netcore》笔记
    • 《设计模式》
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

moklgy docs

全栈初级开发工程师
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 后端文章

    • 技术题
  • .netcore

    • 《asp.netcore》笔记
    • 《设计模式》
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 依赖注入
  • 中间件基础
  • 中间件高级
    • 1. 请求重试中间件
    • 2. 审计日志中间件
    • 3. 数据脱敏中间件
    • 4. 多租户中间件
    • 5. 特性标记与功能开关中间件
    • 6. 请求队列与批处理中间件
    • 7. 实时性能监控中间件
    • 8. 智能机器人检测中间件
    • 9. 服务降级与熔断中间件
    • 10. 分布式追踪中间件
    • 11. 地理位置感知中间件
    • 12. 静态资源优化中间件
    • 13. 文件上传处理中间件
    • 14. API使用统计中间件
    • 15. 国际地址格式化中间件
    • 16. 内容协商中间件
    • 17. 接口规范强制中间件
    • 18. 自动HTTP缓存控制中间件
    • 19. 强制HTTPS和HSTS中间件
    • 20. 智能返回文件中间件
    • 总结
  • netcore》笔记
moklgydocs
2025-10-12
目录

中间件高级

# 20个实用的ASP.NET Core中间件及其实际应用场景

作为.NET Core专家,我将为您提供20个与前面不重复的中间件实现,结合实际应用场景和与市场上现有解决方案的集成可能性。这些中间件按照复杂性从简单到复杂排列。

# 1. 请求重试中间件

用途: 自动重试因临时错误失败的请求,增强系统弹性。

实现:

public class RetryOptions
{
    public int MaxRetryCount { get; set; } = 3;
    public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMilliseconds(100);
    public bool UseExponentialBackoff { get; set; } = true;
    public List<HttpStatusCode> RetryableStatusCodes { get; set; } = new List<HttpStatusCode>
    {
        HttpStatusCode.RequestTimeout,
        HttpStatusCode.InternalServerError,
        HttpStatusCode.BadGateway,
        HttpStatusCode.ServiceUnavailable,
        HttpStatusCode.GatewayTimeout
    };
    public List<string> RetryablePaths { get; set; } = new List<string>();
}

public class RetryMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RetryOptions _options;
    private readonly ILogger<RetryMiddleware> _logger;

    public RetryMiddleware(RequestDelegate next, RetryOptions options, ILogger<RetryMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!ShouldRetry(context.Request))
        {
            await _next(context);
            return;
        }

        int retryCount = 0;
        bool shouldRetry;

        do
        {
            // 保存原始响应体流并创建一个可重置的内存流
            var originalBodyStream = context.Response.Body;
            using var memoryStream = new MemoryStream();
            context.Response.Body = memoryStream;

            try
            {
                await _next(context);

                // 检查是否需要重试
                shouldRetry = _options.RetryableStatusCodes.Contains((HttpStatusCode)context.Response.StatusCode) && 
                              retryCount < _options.MaxRetryCount;

                if (!shouldRetry)
                {
                    // 将内存流内容复制到原始响应流
                    memoryStream.Position = 0;
                    await memoryStream.CopyToAsync(originalBodyStream);
                    return;
                }

                // 计算延迟时间
                var delay = CalculateDelay(retryCount);
                retryCount++;

                _logger.LogWarning(
                    "Request to {Path} failed with status code {StatusCode}. Retrying ({RetryCount}/{MaxRetryCount}) after {Delay}ms", 
                    context.Request.Path, context.Response.StatusCode, retryCount, _options.MaxRetryCount, delay.TotalMilliseconds);

                // 延迟一段时间后重试
                await Task.Delay(delay);

                // 重置响应
                context.Response.Clear();
            }
            finally
            {
                // 确保最终使用原始响应流
                context.Response.Body = originalBodyStream;
            }
        } 
        while (shouldRetry);
    }

    private bool ShouldRetry(HttpRequest request)
    {
        // 只对GET、HEAD等幂等方法启用重试
        if (!(HttpMethods.IsGet(request.Method) || 
              HttpMethods.IsHead(request.Method) || 
              HttpMethods.IsOptions(request.Method)))
        {
            return false;
        }

        // 如果指定了可重试路径,则检查当前路径是否匹配
        if (_options.RetryablePaths.Count > 0)
        {
            return _options.RetryablePaths.Any(p => 
                request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase));
        }

        return true;
    }

    private TimeSpan CalculateDelay(int retryCount)
    {
        if (_options.UseExponentialBackoff)
        {
            // 指数退避算法: initialDelay * 2^retryCount
            return TimeSpan.FromMilliseconds(
                _options.InitialDelay.TotalMilliseconds * Math.Pow(2, retryCount));
        }

        return _options.InitialDelay;
    }
}

public static class RetryMiddlewareExtensions
{
    public static IApplicationBuilder UseRetry(
        this IApplicationBuilder builder,
        Action<RetryOptions> configureOptions = null)
    {
        var options = new RetryOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<RetryMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

应用场景: 集成不稳定的第三方API时,自动重试可以显著提高成功率。例如,在支付处理系统中集成多个支付网关时,网络偶尔抖动不会导致交易失败。

市场集成: 与Polly库集成,可以利用其更高级的重试策略。

# 2. 审计日志中间件

用途: 记录敏感操作的详细审计跟踪,满足合规性要求。

实现:

public class AuditLogOptions
{
    public bool LogRequestBody { get; set; } = true;
    public bool LogResponseBody { get; set; } = true;
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public List<string> ExcludedBodyContentTypes { get; set; } = new List<string>
    {
        "application/octet-stream",
        "image/",
        "video/",
        "audio/"
    };
    public List<string> SensitiveHeaders { get; set; } = new List<string>
    {
        "Authorization",
        "Cookie",
        "X-API-Key"
    };
    public Func<HttpContext, bool> ShouldAudit { get; set; }
    public Func<HttpContext, string, string> SanitizeBody { get; set; }
}

public class AuditLogMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AuditLogOptions _options;
    private readonly ILogger<AuditLogMiddleware> _logger;

    public AuditLogMiddleware(
        RequestDelegate next,
        AuditLogOptions options,
        ILogger<AuditLogMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否需要审计该请求
        if (ShouldSkipAudit(context))
        {
            await _next(context);
            return;
        }

        // 创建审计记录
        var auditRecord = await CreateAuditRecordAsync(context);
        
        // 对于响应审计,需要缓冲响应
        var originalBodyStream = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            var sw = Stopwatch.StartNew();
            await _next(context);
            sw.Stop();
            
            // 记录响应信息
            auditRecord.StatusCode = context.Response.StatusCode;
            auditRecord.ResponseTime = sw.ElapsedMilliseconds;
            auditRecord.ResponseHeaders = GetHeaders(context.Response.Headers, _options.SensitiveHeaders);
            
            // 记录响应体(如果需要)
            if (_options.LogResponseBody && !IsExcludedContentType(context.Response.ContentType))
            {
                responseBodyStream.Position = 0;
                auditRecord.ResponseBody = await GetBodyAsync(responseBodyStream, context.Response.ContentType);
                
                // 应用自定义消毒函数
                if (_options.SanitizeBody != null)
                {
                    auditRecord.ResponseBody = _options.SanitizeBody(context, auditRecord.ResponseBody);
                }
            }
            
            // 将响应流内容写回原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBodyStream);
        }
        catch (Exception ex)
        {
            auditRecord.Exception = ex.ToString();
            throw;
        }
        finally
        {
            context.Response.Body = originalBodyStream;
            
            // 记录完整的审计信息
            LogAuditRecord(auditRecord);
        }
    }

    private bool ShouldSkipAudit(HttpContext context)
    {
        // 检查排除路径
        if (_options.ExcludedPaths.Any(path => 
            context.Request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }
        
        // 使用自定义审计逻辑(如果提供)
        if (_options.ShouldAudit != null)
        {
            return !_options.ShouldAudit(context);
        }
        
        return false;
    }

    private async Task<AuditRecord> CreateAuditRecordAsync(HttpContext context)
    {
        var auditRecord = new AuditRecord
        {
            TraceId = Activity.Current?.Id ?? context.TraceIdentifier,
            Timestamp = DateTimeOffset.UtcNow,
            UserId = context.User?.Identity?.IsAuthenticated == true 
                ? context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value 
                : null,
            Username = context.User?.Identity?.Name,
            IpAddress = context.Connection.RemoteIpAddress?.ToString(),
            UserAgent = context.Request.Headers["User-Agent"],
            Method = context.Request.Method,
            Path = context.Request.Path,
            QueryString = context.Request.QueryString.ToString(),
            RequestHeaders = GetHeaders(context.Request.Headers, _options.SensitiveHeaders)
        };
        
        // 记录请求体(如果需要)
        if (_options.LogRequestBody && 
            context.Request.ContentLength > 0 && 
            !IsExcludedContentType(context.Request.ContentType))
        {
            context.Request.EnableBuffering();
            auditRecord.RequestBody = await GetBodyAsync(context.Request.Body, context.Request.ContentType);
            
            // 应用自定义消毒函数
            if (_options.SanitizeBody != null)
            {
                auditRecord.RequestBody = _options.SanitizeBody(context, auditRecord.RequestBody);
            }
            
            // 重置请求体位置,以便后续中间件读取
            context.Request.Body.Position = 0;
        }
        
        return auditRecord;
    }

    private bool IsExcludedContentType(string contentType)
    {
        if (string.IsNullOrEmpty(contentType))
            return false;
            
        return _options.ExcludedBodyContentTypes.Any(excluded => 
            contentType.StartsWith(excluded, StringComparison.OrdinalIgnoreCase));
    }

    private Dictionary<string, string> GetHeaders(IHeaderDictionary headers, List<string> sensitiveHeaders)
    {
        var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        
        foreach (var header in headers)
        {
            var headerName = header.Key;
            var headerValue = header.Value.ToString();
            
            // 对敏感头部进行混淆
            if (sensitiveHeaders.Any(h => headerName.Equals(h, StringComparison.OrdinalIgnoreCase)))
            {
                headerValue = "[REDACTED]";
            }
            
            result[headerName] = headerValue;
        }
        
        return result;
    }

    private async Task<string> GetBodyAsync(Stream bodyStream, string contentType)
    {
        // 保存当前位置
        var position = bodyStream.Position;
        bodyStream.Position = 0;
        
        try
        {
            // 最多读取前8KB
            const int maxBodyLength = 8192;
            var buffer = new byte[Math.Min(maxBodyLength, bodyStream.Length > 0 ? (int)bodyStream.Length : maxBodyLength)];
            
            await bodyStream.ReadAsync(buffer, 0, buffer.Length);
            
            // 根据内容类型转换
            if (!string.IsNullOrEmpty(contentType) && contentType.Contains("application/json"))
            {
                return Encoding.UTF8.GetString(buffer).Trim('\0');
            }
            else if (!string.IsNullOrEmpty(contentType) && contentType.Contains("text/"))
            {
                return Encoding.UTF8.GetString(buffer).Trim('\0');
            }
            else
            {
                return "[Binary data]";
            }
        }
        finally
        {
            // 恢复原始位置
            bodyStream.Position = position;
        }
    }

    private void LogAuditRecord(AuditRecord record)
    {
        // 你可以在这里将审计记录写入数据库、发送到消息队列等
        // 在这个示例中,我们简单地写入日志
        var json = JsonSerializer.Serialize(record, new JsonSerializerOptions 
        { 
            WriteIndented = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        });
        
        _logger.LogInformation("Audit Log: {AuditRecord}", json);
    }

    private class AuditRecord
    {
        public string TraceId { get; set; }
        public DateTimeOffset Timestamp { get; set; }
        public string UserId { get; set; }
        public string Username { get; set; }
        public string IpAddress { get; set; }
        public string UserAgent { get; set; }
        public string Method { get; set; }
        public string Path { get; set; }
        public string QueryString { get; set; }
        public Dictionary<string, string> RequestHeaders { get; set; }
        public string RequestBody { get; set; }
        public int StatusCode { get; set; }
        public long ResponseTime { get; set; }
        public Dictionary<string, string> ResponseHeaders { get; set; }
        public string ResponseBody { get; set; }
        public string Exception { get; set; }
    }
}

public static class AuditLogMiddlewareExtensions
{
    public static IApplicationBuilder UseAuditLog(
        this IApplicationBuilder builder,
        Action<AuditLogOptions> configureOptions = null)
    {
        var options = new AuditLogOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<AuditLogMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265

应用场景: 金融、医疗和政府应用需要详细记录谁做了什么操作,何时操作,以及如何操作,以满足法规要求。

市场集成: 与Serilog、Elasticsearch和Kibana集成,创建可搜索的审计日志存储和分析系统。

# 3. 数据脱敏中间件

用途: 自动检测和脱敏响应中的敏感数据,如信用卡号、SSN等。

实现:

public class DataMaskingOptions
{
    public List<MaskingRule> Rules { get; set; } = new List<MaskingRule>();
    public bool EnableJsonMasking { get; set; } = true;
    public List<string> IncludedContentTypes { get; set; } = new List<string>
    {
        "application/json",
        "text/plain",
        "text/html",
        "application/xml",
        "text/xml"
    };
}

public class MaskingRule
{
    public string Pattern { get; set; }
    public string Replacement { get; set; }
    public string Description { get; set; }
    
    public MaskingRule(string pattern, string replacement, string description)
    {
        Pattern = pattern;
        Replacement = replacement;
        Description = description;
    }
}

public class DataMaskingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly DataMaskingOptions _options;
    private readonly ILogger<DataMaskingMiddleware> _logger;
    private readonly List<(Regex Regex, string Replacement, string Description)> _compiledRules;

    public DataMaskingMiddleware(
        RequestDelegate next,
        DataMaskingOptions options,
        ILogger<DataMaskingMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 预编译正则表达式
        _compiledRules = _options.Rules.Select(r => (
            new Regex(r.Pattern, RegexOptions.Compiled),
            r.Replacement,
            r.Description
        )).ToList();
        
        // 如果没有规则,添加默认规则
        if (_compiledRules.Count == 0)
        {
            AddDefaultRules();
        }
    }

    private void AddDefaultRules()
    {
        // 信用卡号: 只显示最后4位
        _compiledRules.Add((
            new Regex(@"\b(?:\d[ -]*?){13,16}\b", RegexOptions.Compiled),
            m => MaskAllButLast4(m.Value),
            "Credit Card Number"
        ));
        
        // 社会安全号(SSN): 只显示最后4位
        _compiledRules.Add((
            new Regex(@"\b\d{3}[-]?\d{2}[-]?\d{4}\b", RegexOptions.Compiled),
            m => "XXX-XX-" + m.Value.Replace("-", "").Substring(5),
            "Social Security Number"
        ));
        
        // 电子邮件: 遮盖用户名部分
        _compiledRules.Add((
            new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", RegexOptions.Compiled),
            m => MaskEmail(m.Value),
            "Email Address"
        ));
        
        // 电话号码: 只显示最后4位
        _compiledRules.Add((
            new Regex(@"\b(?:\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b", RegexOptions.Compiled),
            m => MaskPhone(m.Value),
            "Phone Number"
        ));
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var originalBodyStream = context.Response.Body;

        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;

        try
        {
            await _next(context);

            if (ShouldMaskResponse(context.Response))
            {
                // 读取响应内容
                responseBodyStream.Position = 0;
                var responseBody = await new StreamReader(responseBodyStream).ReadToEndAsync();

                // 执行数据脱敏
                var maskedResponseBody = MaskSensitiveData(responseBody, context.Response.ContentType);

                // 用脱敏后的内容替换原响应
                var maskedBytes = Encoding.UTF8.GetBytes(maskedResponseBody);
                context.Response.ContentLength = maskedBytes.Length;
                
                responseBodyStream.SetLength(0);
                await responseBodyStream.WriteAsync(maskedBytes, 0, maskedBytes.Length);
            }

            // 将响应内容复制到原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBodyStream);
        }
        finally
        {
            context.Response.Body = originalBodyStream;
        }
    }

    private bool ShouldMaskResponse(HttpResponse response)
    {
        // 检查响应是否包含内容
        if (response.ContentLength == 0)
            return false;
            
        // 检查内容类型是否应该被脱敏
        var contentType = response.ContentType ?? string.Empty;
        
        return _options.IncludedContentTypes.Any(includedType => 
            contentType.StartsWith(includedType, StringComparison.OrdinalIgnoreCase));
    }

    private string MaskSensitiveData(string content, string contentType)
    {
        if (string.IsNullOrEmpty(content))
            return content;
            
        // 对于JSON内容,尝试使用结构化方法脱敏
        if (_options.EnableJsonMasking && 
            !string.IsNullOrEmpty(contentType) && 
            contentType.Contains("application/json"))
        {
            try
            {
                return MaskJsonContent(content);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Error masking JSON content, falling back to regex masking");
                // 降级到正则表达式脱敏
            }
        }
        
        // 使用正则表达式脱敏
        foreach (var (regex, replacement, description) in _compiledRules)
        {
            try
            {
                content = regex.Replace(content, new MatchEvaluator(m => 
                {
                    var replaced = replacement is string repStr ? repStr : ((Func<Match, string>)replacement)(m);
                    _logger.LogDebug("Masked {Type}: {Original} -> {Masked}", description, m.Value, replaced);
                    return replaced;
                }));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error applying masking rule {Description}", description);
            }
        }
        
        return content;
    }

    private string MaskJsonContent(string jsonContent)
    {
        var document = JsonDocument.Parse(jsonContent);
        using var outputStream = new MemoryStream();
        using var writer = new Utf8JsonWriter(outputStream, new JsonWriterOptions { Indented = true });
        
        MaskJsonElement(document.RootElement, writer);
        
        writer.Flush();
        return Encoding.UTF8.GetString(outputStream.ToArray());
    }

    private void MaskJsonElement(JsonElement element, Utf8JsonWriter writer)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                writer.WriteStartObject();
                foreach (var property in element.EnumerateObject())
                {
                    writer.WritePropertyName(property.Name);
                    
                    // 检查属性名是否敏感(如creditCard, ssn等)
                    var isSensitiveProperty = IsSensitivePropertyName(property.Name);
                    
                    if (isSensitiveProperty && property.Value.ValueKind == JsonValueKind.String)
                    {
                        var value = property.Value.GetString();
                        var masked = MaskSensitiveValue(value, property.Name);
                        writer.WriteStringValue(masked);
                    }
                    else
                    {
                        MaskJsonElement(property.Value, writer);
                    }
                }
                writer.WriteEndObject();
                break;
                
            case JsonValueKind.Array:
                writer.WriteStartArray();
                foreach (var item in element.EnumerateArray())
                {
                    MaskJsonElement(item, writer);
                }
                writer.WriteEndArray();
                break;
                
            case JsonValueKind.String:
                var stringValue = element.GetString();
                var maskedValue = MaskSensitiveData(stringValue, "text/plain");
                writer.WriteStringValue(maskedValue);
                break;
                
            default:
                // 对于其他类型,直接复制值
                element.WriteTo(writer);
                break;
        }
    }

    private bool IsSensitivePropertyName(string propertyName)
    {
        // 检查属性名是否包含敏感信息关键词
        var sensitiveKeywords = new[]
        {
            "password", "pwd", "secret", "creditcard", "cardnumber", "credit_card", "credit-card",
            "ssn", "socialsecurity", "social-security", "tax", "passport", "email", "phone",
            "address", "token", "key", "auth", "pin", "account"
        };
        
        return sensitiveKeywords.Any(keyword => 
            propertyName.Contains(keyword, StringComparison.OrdinalIgnoreCase));
    }

    private string MaskSensitiveValue(string value, string propertyName)
    {
        if (string.IsNullOrEmpty(value))
            return value;
            
        // 根据属性名选择适当的脱敏方法
        if (propertyName.Contains("email", StringComparison.OrdinalIgnoreCase))
        {
            return MaskEmail(value);
        }
        else if (propertyName.Contains("card", StringComparison.OrdinalIgnoreCase) || 
                 propertyName.Contains("credit", StringComparison.OrdinalIgnoreCase))
        {
            return MaskAllButLast4(value);
        }
        else if (propertyName.Contains("phone", StringComparison.OrdinalIgnoreCase))
        {
            return MaskPhone(value);
        }
        else if (propertyName.Contains("ssn", StringComparison.OrdinalIgnoreCase) || 
                 propertyName.Contains("social", StringComparison.OrdinalIgnoreCase))
        {
            var clean = value.Replace("-", "").Replace(" ", "");
            return clean.Length >= 9 ? "XXX-XX-" + clean.Substring(clean.Length - 4) : "XXX-XX-XXXX";
        }
        else if (propertyName.Contains("password", StringComparison.OrdinalIgnoreCase) || 
                 propertyName.Contains("secret", StringComparison.OrdinalIgnoreCase) || 
                 propertyName.Contains("key", StringComparison.OrdinalIgnoreCase) ||
                 propertyName.Contains("token", StringComparison.OrdinalIgnoreCase))
        {
            return "********";
        }
        
        // 默认脱敏规则:保留前2个和后2个字符
        if (value.Length > 4)
        {
            return value.Substring(0, 2) + new string('*', value.Length - 4) + value.Substring(value.Length - 2);
        }
        
        return new string('*', value.Length);
    }

    // 只显示最后4位
    private static string MaskAllButLast4(string value)
    {
        var clean = value.Replace("-", "").Replace(" ", "");
        if (clean.Length <= 4)
            return new string('*', clean.Length);
            
        return new string('*', clean.Length - 4) + clean.Substring(clean.Length - 4);
    }

    // 遮盖电子邮件
    private static string MaskEmail(string email)
    {
        var parts = email.Split('@');
        if (parts.Length != 2)
            return email;
            
        var username = parts[0];
        var domain = parts[1];
        
        if (username.Length <= 2)
            return new string('*', username.Length) + "@" + domain;
            
        return username.Substring(0, 2) + new string('*', username.Length - 2) + "@" + domain;
    }

    // 遮盖电话号码
    private static string MaskPhone(string phone)
    {
        // 清除非数字字符
        var clean = new string(phone.Where(char.IsDigit).ToArray());
        
        if (clean.Length <= 4)
            return phone;
            
        // 保留最后4位数字
        var last4 = clean.Substring(clean.Length - 4);
        
        // 根据原始格式构建遮盖后的号码
        var result = phone;
        int digitCount = 0;
        
        for (int i = 0; i < result.Length; i++)
        {
            if (char.IsDigit(result[i]))
            {
                digitCount++;
                if (digitCount <= clean.Length - 4)
                {
                    result = result.Remove(i, 1).Insert(i, "*");
                }
            }
        }
        
        return result;
    }
}

public static class DataMaskingMiddlewareExtensions
{
    public static IApplicationBuilder UseDataMasking(
        this IApplicationBuilder builder,
        Action<DataMaskingOptions> configureOptions = null)
    {
        var options = new DataMaskingOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<DataMaskingMiddleware>(options);
    }
    
    // 常用脱敏规则
    public static DataMaskingOptions AddCreditCardMasking(this DataMaskingOptions options)
    {
        options.Rules.Add(new MaskingRule(
            @"\b(?:\d[ -]*?){13,16}\b",
            m => MaskAllButLast4(m.Value),
            "Credit Card Number"
        ));
        return options;
    }
    
    public static DataMaskingOptions AddSsnMasking(this DataMaskingOptions options)
    {
        options.Rules.Add(new MaskingRule(
            @"\b\d{3}[-]?\d{2}[-]?\d{4}\b",
            m => "XXX-XX-" + m.Value.Replace("-", "").Substring(5),
            "Social Security Number"
        ));
        return options;
    }
    
    public static DataMaskingOptions AddEmailMasking(this DataMaskingOptions options)
    {
        options.Rules.Add(new MaskingRule(
            @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
            m => {
                var parts = m.Value.Split('@');
                if (parts.Length != 2) return m.Value;
                var name = parts[0];
                if (name.Length <= 2) return "****@" + parts[1];
                return name.Substring(0, 2) + "****@" + parts[1];
            },
            "Email Address"
        ));
        return options;
    }
    
    private static string MaskAllButLast4(string value)
    {
        var clean = value.Replace("-", "").Replace(" ", "");
        if (clean.Length <= 4)
            return new string('*', clean.Length);
            
        return new string('*', clean.Length - 4) + clean.Substring(clean.Length - 4);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415

注册方式:

app.UseDataMasking(options => {
    options.AddCreditCardMasking();
    options.AddSsnMasking();
    options.AddEmailMasking();
    
    // 自定义脱敏规则
    options.Rules.Add(new MaskingRule(
        @"\b\d{10,11}\b", // 10-11位数字(可能是账号)
        "********", 
        "Account Number"
    ));
});
1
2
3
4
5
6
7
8
9
10
11
12

应用场景: 处理敏感个人信息的应用程序需要确保日志和调试输出不会泄露敏感数据。在金融和医疗应用中尤为重要。

市场集成: 与数据保护工具(如GDPR合规套件)集成,确保数据在整个处理流程中的安全性。

# 4. 多租户中间件

用途: 为多租户SaaS应用提供租户识别和隔离。

实现:

public class MultiTenancyOptions
{
    public TenantResolutionStrategy ResolutionStrategy { get; set; } = TenantResolutionStrategy.Host;
    public string RouteParameterName { get; set; } = "tenant";
    public string HeaderName { get; set; } = "X-Tenant";
    public string QueryStringParameterName { get; set; } = "tenant";
    public string CookieName { get; set; } = "tenant_id";
    public string ClaimType { get; set; } = "tenant_id";
    public bool UseCache { get; set; } = true;
    public TimeSpan CacheExpiration { get; set; } = TimeSpan.FromMinutes(10);
    public Func<HttpContext, Task<TenantContext>> TenantResolver { get; set; }
}

public enum TenantResolutionStrategy
{
    Host,
    Route,
    Header,
    QueryString,
    Cookie,
    Claim,
    Custom
}

public class TenantContext
{
    public string TenantId { get; set; }
    public string Name { get; set; }
    public string ConnectionString { get; set; }
    public Dictionary<string, object> Items { get; } = new Dictionary<string, object>();
}

public interface ITenantContextAccessor
{
    TenantContext TenantContext { get; set; }
}

public class TenantContextAccessor : ITenantContextAccessor
{
    private static readonly AsyncLocal<TenantContextHolder> _tenantContextCurrent = new();
    
    public TenantContext TenantContext
    {
        get => _tenantContextCurrent.Value?.Context;
        set
        {
            if (_tenantContextCurrent.Value == null)
            {
                _tenantContextCurrent.Value = new TenantContextHolder();
            }
            
            _tenantContextCurrent.Value.Context = value;
        }
    }
    
    private class TenantContextHolder
    {
        public TenantContext Context;
    }
}

public interface ITenantStore
{
    Task<TenantContext> GetTenantAsync(string identifier);
}

public class InMemoryTenantStore : ITenantStore
{
    private readonly Dictionary<string, TenantContext> _tenants = new(StringComparer.OrdinalIgnoreCase);
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheExpiration;
    
    public InMemoryTenantStore(IMemoryCache cache, TimeSpan cacheExpiration)
    {
        _cache = cache;
        _cacheExpiration = cacheExpiration;
        
        // 添加一些示例租户
        AddTenant("tenant1", "Tenant One", "Server=server1;Database=Tenant1Db;Trusted_Connection=True;");
        AddTenant("tenant2", "Tenant Two", "Server=server1;Database=Tenant2Db;Trusted_Connection=True;");
        AddTenant("tenant3", "Tenant Three", "Server=server1;Database=Tenant3Db;Trusted_Connection=True;");
    }
    
    public void AddTenant(string id, string name, string connectionString)
    {
        _tenants[id] = new TenantContext
        {
            TenantId = id,
            Name = name,
            ConnectionString = connectionString
        };
    }
    
    public Task<TenantContext> GetTenantAsync(string identifier)
    {
        if (string.IsNullOrEmpty(identifier))
            return Task.FromResult<TenantContext>(null);

        // 尝试从缓存获取
        var cacheKey = $"tenant:{identifier}";
        if (_cache.TryGetValue(cacheKey, out TenantContext cachedTenant))
        {
            return Task.FromResult(cachedTenant);
        }
            
        // 从存储中获取
        if (_tenants.TryGetValue(identifier, out var tenant))
        {
            // 添加到缓存
            _cache.Set(cacheKey, tenant, _cacheExpiration);
            return Task.FromResult(tenant);
        }
            
        return Task.FromResult<TenantContext>(null);
    }
}

public class MultiTenancyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly MultiTenancyOptions _options;
    private readonly ILogger<MultiTenancyMiddleware> _logger;
    private readonly ITenantStore _tenantStore;

    public MultiTenancyMiddleware(
        RequestDelegate next,
        MultiTenancyOptions options,
        ITenantStore tenantStore,
        ILogger<MultiTenancyMiddleware> logger)
    {
        _next = next;
        _options = options;
        _tenantStore = tenantStore;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, ITenantContextAccessor tenantContextAccessor)
    {
        // 解析租户标识符
        string tenantIdentifier = await ResolveTenantIdentifierAsync(context);
        
        if (!string.IsNullOrEmpty(tenantIdentifier))
        {
            // 获取租户上下文
            var tenantContext = await GetTenantContextAsync(tenantIdentifier);
            
            if (tenantContext != null)
            {
                // 设置租户上下文
                tenantContextAccessor.TenantContext = tenantContext;
                
                // 将租户ID添加到HttpContext中,方便其他中间件和控制器访问
                context.Items["TenantContext"] = tenantContext;
                
                _logger.LogInformation("Tenant resolved: {TenantId} ({TenantName})", 
                    tenantContext.TenantId, tenantContext.Name);
            }
            else
            {
                _logger.LogWarning("Tenant not found: {TenantIdentifier}", tenantIdentifier);
            }
        }
        
        await _next(context);
    }

    private async Task<string> ResolveTenantIdentifierAsync(HttpContext context)
    {
        // 使用自定义解析器
        if (_options.ResolutionStrategy == TenantResolutionStrategy.Custom && _options.TenantResolver != null)
        {
            var tenantContext = await _options.TenantResolver(context);
            return tenantContext?.TenantId;
        }
        
        // 使用内置策略
        return _options.ResolutionStrategy switch
        {
            TenantResolutionStrategy.Host => ResolveFromHost(context),
            TenantResolutionStrategy.Route => ResolveFromRoute(context),
            TenantResolutionStrategy.Header => ResolveFromHeader(context),
            TenantResolutionStrategy.QueryString => ResolveFromQueryString(context),
            TenantResolutionStrategy.Cookie => ResolveFromCookie(context),
            TenantResolutionStrategy.Claim => ResolveFromClaim(context),
            _ => null
        };
    }
    
    private string ResolveFromHost(HttpContext context)
    {
        var host = context.Request.Host.Host;
        
        // 假设格式为: {tenant}.example.com
        var parts = host.Split('.');
        if (parts.Length >= 3)
        {
            return parts[0];
        }
        
        return null;
    }
    
    private string ResolveFromRoute(HttpContext context)
    {
        if (context.Request.RouteValues.TryGetValue(_options.RouteParameterName, out var tenant))
        {
            return tenant?.ToString();
        }
        
        return null;
    }
    
    private string ResolveFromHeader(HttpContext context)
    {
        if (context.Request.Headers.TryGetValue(_options.HeaderName, out var values))
        {
            return values.FirstOrDefault();
        }
        
        return null;
    }
    
    private string ResolveFromQueryString(HttpContext context)
    {
        if (context.Request.Query.TryGetValue(_options.QueryStringParameterName, out var values))
        {
            return values.FirstOrDefault();
        }
        
        return null;
    }
    
    private string ResolveFromCookie(HttpContext context)
    {
        if (context.Request.Cookies.TryGetValue(_options.CookieName, out var value))
        {
            return value;
        }
        
        return null;
    }
    
    private string ResolveFromClaim(HttpContext context)
    {
        return context.User?.FindFirst(_options.ClaimType)?.Value;
    }
    
    private async Task<TenantContext> GetTenantContextAsync(string identifier)
    {
        return await _tenantStore.GetTenantAsync(identifier);
    }
}

public static class MultiTenancyMiddlewareExtensions
{
    public static IServiceCollection AddMultiTenancy(this IServiceCollection services, Action<MultiTenancyOptions> configureOptions = null)
    {
        var options = new MultiTenancyOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        services.AddSingleton<ITenantContextAccessor, TenantContextAccessor>();
        
        // 添加内存缓存(如果启用缓存)
        if (options.UseCache)
        {
            services.AddMemoryCache();
        }
        
        // 注册默认的租户存储
        services.AddSingleton<ITenantStore>(sp => new InMemoryTenantStore(
            sp.GetRequiredService<IMemoryCache>(),
            options.CacheExpiration
        ));
        
        return services;
    }
    
    public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MultiTenancyMiddleware>();
    }
    
    // 获取当前租户上下文的辅助方法
    public static TenantContext GetTenantContext(this HttpContext context)
    {
        if (context.Items.TryGetValue("TenantContext", out var tenantContext))
        {
            return tenantContext as TenantContext;
        }
        
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294

注册方式:

// 在Startup.ConfigureServices中
services.AddMultiTenancy(options => {
    options.ResolutionStrategy = TenantResolutionStrategy.Host;
    
    // 使用自定义租户解析器
    options.TenantResolver = async context => {
        // 复杂的租户解析逻辑
        var tenantId = context.Request.Headers["X-Tenant"].FirstOrDefault() ?? 
                       context.Request.Query["tenant"].FirstOrDefault();
                       
        if (string.IsNullOrEmpty(tenantId))
        {
            return null;
        }
        
        // 例如:查询数据库获取租户信息
        // var tenantRepo = context.RequestServices.GetRequiredService<ITenantRepository>();
        // return await tenantRepo.GetByIdAsync(tenantId);
        
        // 简单示例
        return new TenantContext { TenantId = tenantId };
    };
});

// 在Startup.Configure中(应尽早注册,在认证之后,授权之前)
app.UseMultiTenancy();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

使用示例:

// 在控制器中访问租户信息
public class ProductsController : ControllerBase
{
    private readonly ITenantContextAccessor _tenantContextAccessor;
    
    public ProductsController(ITenantContextAccessor tenantContextAccessor)
    {
        _tenantContextAccessor = tenantContextAccessor;
    }
    
    [HttpGet]
    public IActionResult Get()
    {
        var tenant = _tenantContextAccessor.TenantContext;
        
        // 或者从HttpContext获取
        var tenantFromContext = HttpContext.GetTenantContext();
        
        return Ok(new { 
            TenantId = tenant.TenantId,
            TenantName = tenant.Name,
            Products = GetProductsForTenant(tenant.TenantId)
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

应用场景: SaaS应用需要支持多租户架构,每个租户都有独立的数据和配置。例如CRM、ERP、票务系统等。

市场集成: 与EntityFramework Core的多租户过滤器集成,自动隔离租户数据;与IdentityServer集成,实现多租户认证。

# 5. 特性标记与功能开关中间件

用途: 实现特性标记(Feature Flags)功能,支持逐步发布、A/B测试和功能切换。

实现:

public class FeatureFlagOptions
{
    public string ProviderType { get; set; } = "Memory";
    public TimeSpan CacheExpiration { get; set; } = TimeSpan.FromMinutes(5);
    public List<FeatureDefinition> DefaultFeatures { get; set; } = new List<FeatureDefinition>();
}

public class FeatureDefinition
{
    public string Name { get; set; }
    public bool Enabled { get; set; }
    public Dictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>();
    public List<string> EnabledForUsers { get; set; } = new List<string>();
    public List<string> EnabledForRoles { get; set; } = new List<string>();
    public List<string> EnabledForTenants { get; set; } = new List<string>();
    public int RolloutPercentage { get; set; } = 100;
    public DateTime? ExpiresOn { get; set; }
}

public interface IFeatureManager
{
    Task<bool> IsEnabledAsync(string featureName);
    Task<bool> IsEnabledAsync(string featureName, string userId);
    Task<T> GetFeatureParameterAsync<T>(string featureName, string parameterName, T defaultValue = default);
}

public interface IFeatureProvider
{
    Task<IEnumerable<FeatureDefinition>> GetAllFeaturesAsync();
    Task<FeatureDefinition> GetFeatureAsync(string featureName);
}

public class InMemoryFeatureProvider : IFeatureProvider
{
    private readonly ConcurrentDictionary<string, FeatureDefinition> _features = new ConcurrentDictionary<string, FeatureDefinition>(StringComparer.OrdinalIgnoreCase);
    
    public InMemoryFeatureProvider(IEnumerable<FeatureDefinition> initialFeatures = null)
    {
        if (initialFeatures != null)
        {
            foreach (var feature in initialFeatures)
            {
                _features[feature.Name] = feature;
            }
        }
    }
    
    public Task<IEnumerable<FeatureDefinition>> GetAllFeaturesAsync()
    {
        return Task.FromResult<IEnumerable<FeatureDefinition>>(_features.Values);
    }
    
    public Task<FeatureDefinition> GetFeatureAsync(string featureName)
    {
        _features.TryGetValue(featureName, out var feature);
        return Task.FromResult(feature);
    }
    
    public void AddOrUpdateFeature(FeatureDefinition feature)
    {
        _features[feature.Name] = feature;
    }
    
    public bool RemoveFeature(string featureName)
    {
        return _features.TryRemove(featureName, out _);
    }
}

public class FeatureManager : IFeatureManager
{
    private readonly IFeatureProvider _provider;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheExpiration;
    private readonly ILogger<FeatureManager> _logger;
    
    public FeatureManager(
        IFeatureProvider provider,
        IHttpContextAccessor httpContextAccessor,
        IMemoryCache cache,
        TimeSpan cacheExpiration,
        ILogger<FeatureManager> logger)
    {
        _provider = provider;
        _httpContextAccessor = httpContextAccessor;
        _cache = cache;
        _cacheExpiration = cacheExpiration;
        _logger = logger;
    }
    
    public async Task<bool> IsEnabledAsync(string featureName)
    {
        var context = _httpContextAccessor.HttpContext;
        var userId = context?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        return await IsEnabledAsync(featureName, userId);
    }
    
    public async Task<bool> IsEnabledAsync(string featureName, string userId)
    {
        var feature = await GetFeatureDefinitionAsync(featureName);
        if (feature == null)
        {
            _logger.LogDebug("Feature {FeatureName} not found", featureName);
            return false;
        }
        
        // 检查功能是否过期
        if (feature.ExpiresOn.HasValue && feature.ExpiresOn.Value < DateTime.UtcNow)
        {
            _logger.LogDebug("Feature {FeatureName} has expired on {ExpiryDate}", 
                featureName, feature.ExpiresOn.Value);
            return false;
        }
        
        // 检查基本启用状态
        if (!feature.Enabled)
        {
            _logger.LogDebug("Feature {FeatureName} is disabled globally", featureName);
            return false;
        }
        
        var context = _httpContextAccessor.HttpContext;
        
        // 检查用户是否明确启用
        if (!string.IsNullOrEmpty(userId) && feature.EnabledForUsers.Contains(userId))
        {
            _logger.LogDebug("Feature {FeatureName} is enabled for user {UserId}", featureName, userId);
            return true;
        }
        
        // 检查角色是否启用
        if (context?.User?.Identity?.IsAuthenticated == true && feature.EnabledForRoles.Count > 0)
        {
            foreach (var role in feature.EnabledForRoles)
            {
                if (context.User.IsInRole(role))
                {
                    _logger.LogDebug("Feature {FeatureName} is enabled for role {Role}", featureName, role);
                    return true;
                }
            }
        }
        
        // 检查租户是否启用
        if (context != null && feature.EnabledForTenants.Count > 0)
        {
            var tenantId = GetCurrentTenantId(context);
            if (!string.IsNullOrEmpty(tenantId) && feature.EnabledForTenants.Contains(tenantId))
            {
                _logger.LogDebug("Feature {FeatureName} is enabled for tenant {TenantId}", featureName, tenantId);
                return true;
            }
        }
        
        // 百分比展示
        if (feature.RolloutPercentage < 100)
        {
            var key = string.IsNullOrEmpty(userId) ? GetClientIdFromRequest() : userId;
            if (!string.IsNullOrEmpty(key))
            {
                var percentage = GetStablePercentage(featureName, key);
                if (percentage > feature.RolloutPercentage)
                {
                    _logger.LogDebug("Feature {FeatureName} is disabled for {Key} due to rollout percentage ({Percentage}% vs {Threshold}%)", 
                        featureName, key, percentage, feature.RolloutPercentage);
                    return false;
                }
            }
        }
        
        _logger.LogDebug("Feature {FeatureName} is enabled by default rules", featureName);
        return true;
    }
    
    public async Task<T> GetFeatureParameterAsync<T>(string featureName, string parameterName, T defaultValue = default)
    {
        var feature = await GetFeatureDefinitionAsync(featureName);
        
        if (feature == null || !feature.Parameters.TryGetValue(parameterName, out var value))
        {
            return defaultValue;
        }
        
        try
        {
            return (T)Convert.ChangeType(value, typeof(T));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error converting parameter {ParameterName} for feature {FeatureName}", 
                parameterName, featureName);
            return defaultValue;
        }
    }
    
    private async Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
    {
        var cacheKey = $"feature:{featureName}";
        
        if (_cache.TryGetValue(cacheKey, out FeatureDefinition cachedFeature))
        {
            return cachedFeature;
        }
        
        var feature = await _provider.GetFeatureAsync(featureName);
        
        if (feature != null)
        {
            _cache.Set(cacheKey, feature, _cacheExpiration);
        }
        
        return feature;
    }
    
    private string GetCurrentTenantId(HttpContext context)
    {
        // 尝试从HttpContext项中获取租户ID,这通常由多租户中间件设置
        if (context.Items.TryGetValue("TenantContext", out var tenantContextObj) && 
            tenantContextObj is TenantContext tenantContext)
        {
            return tenantContext.TenantId;
        }
        
        // 或者从声明中获取
        return context.User?.FindFirst("tenant_id")?.Value;
    }
    
    private string GetClientIdFromRequest()
    {
        var context = _httpContextAccessor.HttpContext;
        if (context == null)
            return null;
            
        // 尝试使用IP地址作为客户端标识符
        return context.Connection.RemoteIpAddress?.ToString();
    }
    
    private int GetStablePercentage(string featureName, string key)
    {
        // 使用确定性哈希函数,确保同一用户始终获得相同的百分比值
        using var md5 = MD5.Create();
        var input = Encoding.UTF8.GetBytes($"{featureName}:{key}");
        var hash = md5.ComputeHash(input);
        
        // 使用哈希的前4个字节作为32位整数
        var number = BitConverter.ToUInt32(hash, 0);
        
        // 将32位整数转换为0-100的范围
        return (int)(number % 100);
    }
}

public class FeatureFlagMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<FeatureFlagMiddleware> _logger;

    public FeatureFlagMiddleware(
        RequestDelegate next,
        ILogger<FeatureFlagMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IFeatureManager featureManager)
    {
        // 查找控制器操作上的FeatureAttribute
        var endpoint = context.GetEndpoint();
        var featureAttributes = endpoint?.Metadata.GetOrderedMetadata<FeatureAttribute>();
        
        if (featureAttributes != null && featureAttributes.Any())
        {
            foreach (var attribute in featureAttributes)
            {
                bool isEnabled = await featureManager.IsEnabledAsync(attribute.FeatureName);
                
                if (!isEnabled)
                {
                    _logger.LogInformation(
                        "Access to endpoint {EndpointName} was denied because feature {FeatureName} is not enabled",
                        endpoint.DisplayName,
                        attribute.FeatureName);
                    
                    context.Response.StatusCode = attribute.StatusCodeIfDisabled;
                    
                    if (!string.IsNullOrEmpty(attribute.RedirectUrlIfDisabled))
                    {
                        context.Response.Redirect(attribute.RedirectUrlIfDisabled);
                    }
                    else if (!string.IsNullOrEmpty(attribute.MessageIfDisabled))
                    {
                        await context.Response.WriteAsync(attribute.MessageIfDisabled);
                    }
                    
                    return;
                }
            }
        }
        
        await _next(context);
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class FeatureAttribute : Attribute
{
    public string FeatureName { get; }
    public int StatusCodeIfDisabled { get; set; } = 404;
    public string RedirectUrlIfDisabled { get; set; }
    public string MessageIfDisabled { get; set; } = "This feature is not available";
    
    public FeatureAttribute(string featureName)
    {
        FeatureName = featureName;
    }
}

public static class FeatureFlagMiddlewareExtensions
{
    public static IServiceCollection AddFeatureManagement(
        this IServiceCollection services,
        Action<FeatureFlagOptions> configureOptions = null)
    {
        var options = new FeatureFlagOptions();
        configureOptions?.Invoke(options);
        
        services.AddHttpContextAccessor();
        services.AddMemoryCache();
        
        // 注册功能提供者
        if (options.ProviderType.Equals("Memory", StringComparison.OrdinalIgnoreCase))
        {
            services.AddSingleton<IFeatureProvider>(sp => new InMemoryFeatureProvider(options.DefaultFeatures));
        }
        else
        {
            // 可以在这里添加其他提供者的支持,如数据库、配置文件等
            throw new NotSupportedException($"Feature provider type '{options.ProviderType}' is not supported");
        }
        
        // 注册功能管理器
        services.AddScoped<IFeatureManager>(sp => new FeatureManager(
            sp.GetRequiredService<IFeatureProvider>(),
            sp.GetRequiredService<IHttpContextAccessor>(),
            sp.GetRequiredService<IMemoryCache>(),
            options.CacheExpiration,
            sp.GetRequiredService<ILogger<FeatureManager>>()
        ));
        
        return services;
    }
    
    public static IApplicationBuilder UseFeatureFlags(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FeatureFlagMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360

注册方式:

// 在Startup.ConfigureServices中
services.AddFeatureManagement(options => {
    options.DefaultFeatures.Add(new FeatureDefinition
    {
        Name = "NewCheckout",
        Enabled = true,
        RolloutPercentage = 50,
        EnabledForRoles = new List<string> { "Admin", "Beta" },
        Parameters = new Dictionary<string, object>
        {
            ["ButtonColor"] = "#FF5733",
            ["ShowNewPromotions"] = true
        }
    });
    
    options.DefaultFeatures.Add(new FeatureDefinition
    {
        Name = "DarkMode",
        Enabled = true,
        EnabledForUsers = new List<string> { "user1", "user2" },
        ExpiresOn = DateTime.UtcNow.AddDays(30) // 30天后过期
    });
});

// 在Startup.Configure中
app.UseFeatureFlags();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

使用示例:

// 使用特性标记控制器或操作
[Feature("NewCheckout", StatusCodeIfDisabled = 302, RedirectUrlIfDisabled = "/coming-soon")]
public class CheckoutController : Controller
{
    private readonly IFeatureManager _featureManager;
    
    public CheckoutController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }
    
    [HttpGet]
    public async Task<IActionResult> Index()
    {
        // 检查某个功能是否启用
        if (await _featureManager.IsEnabledAsync("ExpressCheckout"))
        {
            return View("ExpressCheckout");
        }
        
        // 获取功能参数
        var buttonColor = await _featureManager.GetFeatureParameterAsync<string>("NewCheckout", "ButtonColor", "#007bff");
        
        return View(new CheckoutViewModel
        {
            ButtonColor = buttonColor
        });
    }
    
    // 特定操作的功能标记
    [Feature("GiftCards", MessageIfDisabled = "Gift cards are coming soon!")]
    public IActionResult GiftCards()
    {
        return View();
    }
}

// 在Razor视图中使用
@inject IFeatureManager FeatureManager

@if (await FeatureManager.IsEnabledAsync("DarkMode"))
{
    <link rel="stylesheet" href="~/css/dark-theme.css" />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

应用场景: 实现功能的逐步发布、A/B测试、季节性功能和beta测试。例如,电子商务网站上的新结账流程,或社交媒体应用中的新UI功能。

市场集成: 与Microsoft.FeatureManagement库集成,以及与Azure App Configuration服务集成,实现云端功能管理。

# 6. 请求队列与批处理中间件

用途: 对高成本请求进行排队和批处理,提高系统吞吐量和资源利用率。

实现:

public class RequestQueueOptions
{
    public int MaxConcurrentRequests { get; set; } = 10;
    public int QueueCapacity { get; set; } = 100;
    public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(60);
    public int BatchSize { get; set; } = 5;
    public TimeSpan BatchingInterval { get; set; } = TimeSpan.FromMilliseconds(50);
    public List<string> QueueablePaths { get; set; } = new List<string>();
    public List<string> BatchablePaths { get; set; } = new List<string>();
    public Dictionary<string, RequestQueuePolicy> PathPolicies { get; set; } = new Dictionary<string, RequestQueuePolicy>();
}

public class RequestQueuePolicy
{
    public bool EnableQueuing { get; set; } = true;
    public bool EnableBatching { get; set; } = false;
    public int MaxConcurrentRequests { get; set; } = 5;
    public int BatchSize { get; set; } = 5;
    public TimeSpan BatchingInterval { get; set; } = TimeSpan.FromMilliseconds(50);
}

public class RequestQueueMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RequestQueueOptions _options;
    private readonly ILogger<RequestQueueMiddleware> _logger;
    
    // 并发请求信号量字典
    private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
    
    // 请求批处理缓冲区
    private readonly ConcurrentDictionary<string, BatchProcessor> _batchProcessors = new ConcurrentDictionary<string, BatchProcessor>();

    public RequestQueueMiddleware(
        RequestDelegate next,
        RequestQueueOptions options,
        ILogger<RequestQueueMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 为每个路径策略创建一个信号量
        foreach (var policy in _options.PathPolicies)
        {
            _semaphores[policy.Key] = new SemaphoreSlim(policy.Value.MaxConcurrentRequests, policy.Value.MaxConcurrentRequests);
            
            if (policy.Value.EnableBatching)
            {
                _batchProcessors[policy.Key] = new BatchProcessor(
                    policy.Key,
                    policy.Value.BatchSize,
                    policy.Value.BatchingInterval,
                    ProcessBatchAsync,
                    _logger);
            }
        }
        
        // 创建默认信号量
        _semaphores["default"] = new SemaphoreSlim(_options.MaxConcurrentRequests, _options.MaxConcurrentRequests);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;
        
        // 检查是否应用队列
        if (ShouldQueueRequest(path, out var policyKey))
        {
            await ProcessQueuedRequestAsync(context, policyKey);
        }
        // 检查是否应用批处理
        else if (ShouldBatchRequest(path, out policyKey))
        {
            await ProcessBatchedRequestAsync(context, policyKey);
        }
        else
        {
            // 不需要队列或批处理,直接处理
            await _next(context);
        }
    }

    private bool ShouldQueueRequest(string path, out string policyKey)
    {
        policyKey = "default";
        
        // 检查特定路径策略
        foreach (var policy in _options.PathPolicies)
        {
            if (path.StartsWith(policy.Key, StringComparison.OrdinalIgnoreCase) && policy.Value.EnableQueuing)
            {
                policyKey = policy.Key;
                return true;
            }
        }
        
        // 检查可队列路径
        return _options.QueueablePaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
    }

    private bool ShouldBatchRequest(string path, out string policyKey)
    {
        policyKey = null;
        
        // 检查特定路径策略
        foreach (var policy in _options.PathPolicies)
        {
            if (path.StartsWith(policy.Key, StringComparison.OrdinalIgnoreCase) && policy.Value.EnableBatching)
            {
                policyKey = policy.Key;
                return true;
            }
        }
        
        // 检查可批处理路径
        if (_options.BatchablePaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            policyKey = "default";
            return true;
        }
        
        return false;
    }

    private async Task ProcessQueuedRequestAsync(HttpContext context, string policyKey)
    {
        var semaphore = _semaphores.GetOrAdd(policyKey, _ => new SemaphoreSlim(_options.MaxConcurrentRequests, _options.MaxConcurrentRequests));
        
        var path = context.Request.Path.Value;
        _logger.LogDebug("Queueing request for {Path}", path);
        
        using var cts = new CancellationTokenSource(_options.RequestTimeout);
        var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, context.RequestAborted);
        
        try
        {
            var entered = false;
            try
            {
                entered = await semaphore.WaitAsync(_options.RequestTimeout);
                if (!entered)
                {
                    _logger.LogWarning("Request for {Path} timed out while waiting in queue", path);
                    context.Response.StatusCode = 503; // Service Unavailable
                    await context.Response.WriteAsync("Server is busy. Please try again later.");
                    return;
                }
                
                _logger.LogDebug("Processing queued request for {Path}", path);
                await _next(context);
            }
            finally
            {
                if (entered)
                {
                    semaphore.Release();
                }
            }
        }
        catch (OperationCanceledException ex) when (cts.Token.IsCancellationRequested)
        {
            _logger.LogWarning(ex, "Request for {Path} timed out after {Timeout}ms", path, _options.RequestTimeout.TotalMilliseconds);
            context.Response.StatusCode = 504; // Gateway Timeout
            await context.Response.WriteAsync("Request timed out.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing queued request for {Path}", path);
            throw;
        }
    }

    private async Task ProcessBatchedRequestAsync(HttpContext context, string policyKey)
    {
        // 获取或创建批处理器
        var batchProcessor = _batchProcessors.GetOrAdd(policyKey, key => 
        {
            var policy = _options.PathPolicies.TryGetValue(key, out var p) ? p : null;
            var batchSize = policy?.BatchSize ?? _options.BatchSize;
            var interval = policy?.BatchingInterval ?? _options.BatchingInterval;
            
            return new BatchProcessor(key, batchSize, interval, ProcessBatchAsync, _logger);
        });
        
        _logger.LogDebug("Adding request for {Path} to batch for {PolicyKey}", context.Request.Path, policyKey);
        
        // 创建请求任务源
        var requestTask = new TaskCompletionSource<bool>();
        
        // 保存当前请求的HttpContext
        var batchItem = new BatchItem
        {
            Context = context,
            TaskSource = requestTask
        };
        
        // 添加到批处理队列
        batchProcessor.AddItem(batchItem);
        
        using var cts = new CancellationTokenSource(_options.RequestTimeout);
        cts.Token.Register(() => requestTask.TrySetCanceled());
        context.RequestAborted.Register(() => requestTask.TrySetCanceled());
        
        try
        {
            // 等待批处理完成
            await requestTask.Task;
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("Batched request for {Path} was canceled", context.Request.Path);
            context.Response.StatusCode = 499; // Client Closed Request
        }
    }

    private async Task ProcessBatchAsync(string policyKey, List<BatchItem> batch)
    {
        _logger.LogInformation("Processing batch of {Count} requests for {PolicyKey}", batch.Count, policyKey);
        
        // 对于演示,我们只是依次处理每个请求
        // 在实际应用中,您可能想要实现批量处理逻辑,例如合并数据库查询
        
        var semaphore = _semaphores.GetOrAdd(policyKey, _ => new SemaphoreSlim(_options.MaxConcurrentRequests, _options.MaxConcurrentRequests));
        
        var entered = false;
        try
        {
            entered = await semaphore.WaitAsync(_options.RequestTimeout);
            if (!entered)
            {
                _logger.LogWarning("Batch for {PolicyKey} timed out while waiting in queue", policyKey);
                
                // 将超时状态设置给所有批处理项
                foreach (var item in batch)
                {
                    item.Context.Response.StatusCode = 503; // Service Unavailable
                    await item.Context.Response.WriteAsync("Server is busy. Please try again later.");
                    item.TaskSource.TrySetResult(true);
                }
                
                return;
            }
            
            // 为批处理中的每个请求执行下一个中间件
            foreach (var item in batch)
            {
                try
                {
                    await _next(item.Context);
                    item.TaskSource.TrySetResult(true);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing batch item for {Path}", item.Context.Request.Path);
                    item.TaskSource.TrySetException(ex);
                }
            }
        }
        finally
        {
            if (entered)
            {
                semaphore.Release();
            }
        }
    }

    private class BatchItem
    {
        public HttpContext Context { get; set; }
        public TaskCompletionSource<bool> TaskSource { get; set; }
    }

    private class BatchProcessor
    {
        private readonly string _key;
        private readonly int _batchSize;
        private readonly TimeSpan _interval;
        private readonly Func<string, List<BatchItem>, Task> _processAction;
        private readonly ILogger _logger;
        private readonly List<BatchItem> _buffer = new List<BatchItem>();
        private readonly object _bufferLock = new object();
        private readonly Timer _timer;
        
        public BatchProcessor(
            string key,
            int batchSize,
            TimeSpan interval,
            Func<string, List<BatchItem>, Task> processAction,
            ILogger logger)
        {
            _key = key;
            _batchSize = batchSize;
            _interval = interval;
            _processAction = processAction;
            _logger = logger;
            
            // 创建定时器,但不立即启动
            _timer = new Timer(ProcessBatchCallback, null, Timeout.Infinite, Timeout.Infinite);
        }
        
        public void AddItem(BatchItem item)
        {
            lock (_bufferLock)
            {
                _buffer.Add(item);
                
                // 如果这是第一个项目,启动定时器
                if (_buffer.Count == 1)
                {
                    _timer.Change(_interval, Timeout.InfiniteTimeSpan);
                }
                // 如果达到批量大小,立即处理
                else if (_buffer.Count >= _batchSize)
                {
                    _timer.Change(0, Timeout.InfiniteTimeSpan);
                }
            }
        }
        
        private async void ProcessBatchCallback(object state)
        {
            List<BatchItem> itemsToProcess;
            
            lock (_bufferLock)
            {
                if (_buffer.Count == 0)
                {
                    return;
                }
                
                // 获取当前缓冲区中的所有项
                itemsToProcess = _buffer.ToList();
                _buffer.Clear();
            }
            
            try
            {
                await _processAction(_key, itemsToProcess);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing batch for {Key}", _key);
                
                // 将错误传播给所有项
                foreach (var item in itemsToProcess)
                {
                    item.TaskSource.TrySetException(ex);
                }
            }
        }
    }
}

public static class RequestQueueMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestQueue(
        this IApplicationBuilder builder,
        Action<RequestQueueOptions> configureOptions = null)
    {
        var options = new RequestQueueOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<RequestQueueMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367

注册方式:

app.UseRequestQueue(options => {
    // 需要队列的路径
    options.QueueablePaths.Add("/api/reports");
    options.QueueablePaths.Add("/api/exports");
    
    // 需要批处理的路径
    options.BatchablePaths.Add("/api/analytics/events");
    
    // 特定路径的策略
    options.PathPolicies["/api/reports"] = new RequestQueuePolicy {
        EnableQueuing = true,
        MaxConcurrentRequests = 3,
        EnableBatching = false
    };
    
    options.PathPolicies["/api/analytics/events"] = new RequestQueuePolicy {
        EnableQueuing = false,
        EnableBatching = true,
        BatchSize = 10,
        BatchingInterval = TimeSpan.FromMilliseconds(100)
    };
    
    // 全局设置
    options.MaxConcurrentRequests = 20;
    options.QueueCapacity = 200;
    options.RequestTimeout = TimeSpan.FromSeconds(30);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

应用场景: 用于处理资源密集型操作,如报表生成、数据导出和批量数据处理。例如,分析平台的事件跟踪API或电子商务网站的大型目录更新。

市场集成: 与RabbitMQ、Azure Service Bus或Kafka等消息队列系统集成,以实现更强大的背景作业处理;与.NET Core的BackgroundService集成,处理长时间运行的任务。

# 7. 实时性能监控中间件

用途: 收集请求性能数据,实时监控应用程序的健康状况和性能。

实现:

public class PerformanceMonitorOptions
{
    public int SamplingRate { get; set; } = 100; // 百分比: 100表示记录所有请求
    public bool CollectMemoryMetrics { get; set; } = true;
    public bool CollectGcMetrics { get; set; } = true;
    public bool CollectExceptionMetrics { get; set; } = true;
    public TimeSpan MetricsInterval { get; set; } = TimeSpan.FromSeconds(10);
    public int HistorySize { get; set; } = 100;
    public List<string> ExcludedPaths { get; set; } = new List<string>();
}

public class PerformanceMonitorMiddleware
{
    private readonly RequestDelegate _next;
    private readonly PerformanceMonitorOptions _options;
    private readonly ILogger<PerformanceMonitorMiddleware> _logger;
    private readonly ConcurrentDictionary<string, EndpointMetrics> _endpointMetrics = new ConcurrentDictionary<string, EndpointMetrics>();
    private readonly ConcurrentQueue<RequestMetric> _recentRequests = new ConcurrentQueue<RequestMetric>();
    private readonly Random _random = new Random();
    private readonly Timer _metricsTimer;
    
    private long _totalRequests;
    private long _totalErrors;
    private long _activeRequests;

    public PerformanceMonitorMiddleware(
        RequestDelegate next,
        PerformanceMonitorOptions options,
        ILogger<PerformanceMonitorMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 启动定期收集系统指标的定时器
        _metricsTimer = new Timer(CollectSystemMetrics, null, TimeSpan.Zero, _options.MetricsInterval);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该采样该请求
        if (_random.Next(100) >= _options.SamplingRate || 
            ShouldExclude(context.Request.Path))
        {
            await _next(context);
            return;
        }
        
        // 开始追踪请求
        var requestMetric = new RequestMetric
        {
            Id = Guid.NewGuid().ToString(),
            Path = context.Request.Path,
            Method = context.Request.Method,
            StartTime = DateTime.UtcNow,
            TraceId = Activity.Current?.Id ?? context.TraceIdentifier
        };
        
        // 增加活动请求计数
        Interlocked.Increment(ref _activeRequests);
        
        // 创建用于收集响应大小的自定义响应流
        var originalBody = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            await _next(context);
            
            // 计算请求处理时间
            requestMetric.EndTime = DateTime.UtcNow;
            requestMetric.Duration = requestMetric.EndTime - requestMetric.StartTime;
            requestMetric.StatusCode = context.Response.StatusCode;
            
            // 获取响应大小
            requestMetric.ResponseSize = responseBodyStream.Length;
            
            // 更新成功计数
            if (context.Response.StatusCode < 400)
            {
                requestMetric.IsSuccess = true;
            }
            else
            {
                requestMetric.IsSuccess = false;
                Interlocked.Increment(ref _totalErrors);
            }
            
            // 将响应复制回原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBody);
        }
        catch (Exception ex)
        {
            // 记录异常
            requestMetric.EndTime = DateTime.UtcNow;
            requestMetric.Duration = requestMetric.EndTime - requestMetric.StartTime;
            requestMetric.IsSuccess = false;
            requestMetric.Exception = ex.GetType().Name;
            requestMetric.ExceptionMessage = ex.Message;
            
            Interlocked.Increment(ref _totalErrors);
            
            throw;
        }
        finally
        {
            // 减少活动请求计数
            Interlocked.Decrement(ref _activeRequests);
            
            // 增加总请求计数
            Interlocked.Increment(ref _totalRequests);
            
            // 恢复原始响应流
            context.Response.Body = originalBody;
            
            // 更新端点指标
            UpdateEndpointMetrics(requestMetric);
            
            // 添加到最近请求队列
            AddToRecentRequests(requestMetric);
        }
    }

    private bool ShouldExclude(PathString path)
    {
        return _options.ExcludedPaths.Any(p => 
            path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase));
    }

    private void UpdateEndpointMetrics(RequestMetric metric)
    {
        var key = $"{metric.Method}:{metric.Path}";
        
        var endpointMetric = _endpointMetrics.GetOrAdd(key, _ => new EndpointMetrics
        {
            Path = metric.Path,
            Method = metric.Method
        });
        
        endpointMetric.TotalRequests++;
        endpointMetric.TotalDuration += metric.Duration;
        
        if (!metric.IsSuccess)
        {
            endpointMetric.TotalErrors++;
        }
        
        // 更新最小、最大和平均持续时间
        var durationMs = metric.Duration.TotalMilliseconds;
        
        if (endpointMetric.MinDurationMs == 0 || durationMs < endpointMetric.MinDurationMs)
        {
            endpointMetric.MinDurationMs = durationMs;
        }
        
        if (durationMs > endpointMetric.MaxDurationMs)
        {
            endpointMetric.MaxDurationMs = durationMs;
        }
        
        endpointMetric.AvgDurationMs = endpointMetric.TotalDuration.TotalMilliseconds / endpointMetric.TotalRequests;
        
        // 更新响应大小统计
        endpointMetric.TotalResponseSize += metric.ResponseSize;
        endpointMetric.AvgResponseSize = endpointMetric.TotalResponseSize / endpointMetric.TotalRequests;
    }

    private void AddToRecentRequests(RequestMetric metric)
    {
        _recentRequests.Enqueue(metric);
        
        // 保持队列在指定大小限制内
        while (_recentRequests.Count > _options.HistorySize && _recentRequests.TryDequeue(out _))
        {
            // 丢弃最旧的请求
        }
    }

    private void CollectSystemMetrics(object state)
    {
        try
        {
            var systemMetric = new SystemMetric
            {
                Timestamp = DateTime.UtcNow,
                ActiveRequests = _activeRequests,
                TotalRequests = _totalRequests,
                TotalErrors = _totalErrors
            };
            
            if (_options.CollectMemoryMetrics)
            {
                var process = Process.GetCurrentProcess();
                systemMetric.WorkingSet = process.WorkingSet64;
                systemMetric.PrivateMemory = process.PrivateMemorySize64;
                systemMetric.VirtualMemory = process.VirtualMemorySize64;
            }
            
            if (_options.CollectGcMetrics)
            {
                systemMetric.GcGen0Count = GC.CollectionCount(0);
                systemMetric.GcGen1Count = GC.CollectionCount(1);
                systemMetric.GcGen2Count = GC.CollectionCount(2);
                systemMetric.TotalMemory = GC.GetTotalMemory(false);
            }
            
            // 在实际应用中,您可能想要将这些指标发送到监控系统,
            // 例如Prometheus、Application Insights或自定义仪表板
            
            _logger.LogDebug("System metrics: Active={Active}, Total={Total}, Errors={Errors}, Memory={Memory}MB",
                systemMetric.ActiveRequests,
                systemMetric.TotalRequests,
                systemMetric.TotalErrors,
                systemMetric.WorkingSet / 1024 / 1024);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error collecting system metrics");
        }
    }

    // 获取性能指标的方法,可以通过API端点公开
    public PerformanceMetrics GetMetrics()
    {
        return new PerformanceMetrics
        {
            ActiveRequests = _activeRequests,
            TotalRequests = _totalRequests,
            TotalErrors = _totalErrors,
            Endpoints = _endpointMetrics.Values.ToList(),
            RecentRequests = _recentRequests.ToList(),
            Process = new ProcessMetrics
            {
                WorkingSet = Process.GetCurrentProcess().WorkingSet64,
                PrivateMemory = Process.GetCurrentProcess().PrivateMemorySize64,
                Threads = Process.GetCurrentProcess().Threads.Count,
                CpuUsage = GetCpuUsage(),
                StartTime = Process.GetCurrentProcess().StartTime,
                UpTime = DateTime.Now - Process.GetCurrentProcess().StartTime
            }
        };
    }

    private double GetCpuUsage()
    {
        // 注意: 这个方法在某些环境中可能不准确
        // 在生产环境中,建议使用更可靠的方式获取CPU使用率
        try
        {
            var process = Process.GetCurrentProcess();
            var startTime = DateTime.UtcNow;
            var startCpuUsage = process.TotalProcessorTime;
            
            Thread.Sleep(100); // 短暂延迟以测量CPU使用
            
            var endTime = DateTime.UtcNow;
            var endCpuUsage = process.TotalProcessorTime;
            
            var cpuUsedMs = (endCpuUsage - startCpuUsage).TotalMilliseconds;
            var totalElapsedMs = (endTime - startTime).TotalMilliseconds;
            
            return cpuUsedMs / (Environment.ProcessorCount * totalElapsedMs) * 100;
        }
        catch
        {
            return 0;
        }
    }

    // 指标数据模型
    public class RequestMetric
    {
        public string Id { get; set; }
        public string Path { get; set; }
        public string Method { get; set; }
        public DateTime StartTime { get; set; }
        public DateTime EndTime { get; set; }
        public TimeSpan Duration { get; set; }
        public int StatusCode { get; set; }
        public bool IsSuccess { get; set; }
        public string Exception { get; set; }
        public string ExceptionMessage { get; set; }
        public long ResponseSize { get; set; }
        public string TraceId { get; set; }
    }

    public class EndpointMetrics
    {
        public string Path { get; set; }
        public string Method { get; set; }
        public long TotalRequests { get; set; }
        public long TotalErrors { get; set; }
        public TimeSpan TotalDuration { get; set; }
        public double MinDurationMs { get; set; }
        public double MaxDurationMs { get; set; }
        public double AvgDurationMs { get; set; }
        public long TotalResponseSize { get; set; }
        public double AvgResponseSize { get; set; }
    }

    public class SystemMetric
    {
        public DateTime Timestamp { get; set; }
        public long ActiveRequests { get; set; }
        public long TotalRequests { get; set; }
        public long TotalErrors { get; set; }
        public long WorkingSet { get; set; }
        public long PrivateMemory { get; set; }
        public long VirtualMemory { get; set; }
        public long TotalMemory { get; set; }
        public int GcGen0Count { get; set; }
        public int GcGen1Count { get; set; }
        public int GcGen2Count { get; set; }
    }

    public class ProcessMetrics
    {
        public long WorkingSet { get; set; }
        public long PrivateMemory { get; set; }
        public int Threads { get; set; }
        public double CpuUsage { get; set; }
        public DateTime StartTime { get; set; }
        public TimeSpan UpTime { get; set; }
    }

    public class PerformanceMetrics
    {
        public long ActiveRequests { get; set; }
        public long TotalRequests { get; set; }
        public long TotalErrors { get; set; }
        public List<EndpointMetrics> Endpoints { get; set; }
        public List<RequestMetric> RecentRequests { get; set; }
        public ProcessMetrics Process { get; set; }
    }
}

public static class PerformanceMonitorMiddlewareExtensions
{
    public static IApplicationBuilder UsePerformanceMonitor(
        this IApplicationBuilder builder,
        Action<PerformanceMonitorOptions> configureOptions = null)
    {
        var options = new PerformanceMonitorOptions();
        configureOptions?.Invoke(options);
        
        var middleware = builder.UseMiddleware<PerformanceMonitorMiddleware>(options);
        
        // 注册中间件实例,以便可以从控制器或其他服务访问其方法
        var serviceProvider = builder.ApplicationServices;
        var scope = serviceProvider.CreateScope();
        var instance = scope.ServiceProvider.GetRequiredService<PerformanceMonitorMiddleware>();
        
        return middleware;
    }
    
    // 用于注册性能监控控制器的扩展方法
    public static IServiceCollection AddPerformanceMonitorController(this IServiceCollection services)
    {
        services.AddSingleton<PerformanceMonitorMiddleware>();
        
        return services;
    }
}

// 用于公开性能数据的API控制器
[ApiController]
[Route("api/[controller]")]
public class MetricsController : ControllerBase
{
    private readonly PerformanceMonitorMiddleware _performanceMonitor;
    
    public MetricsController(PerformanceMonitorMiddleware performanceMonitor)
    {
        _performanceMonitor = performanceMonitor;
    }
    
    [HttpGet]
    public IActionResult GetMetrics()
    {
        return Ok(_performanceMonitor.GetMetrics());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384

注册方式:

// 在Startup.ConfigureServices中
services.AddPerformanceMonitorController();

// 在Startup.Configure中
app.UsePerformanceMonitor(options => {
    options.SamplingRate = 50; // 只采样50%的请求
    options.ExcludedPaths.Add("/metrics");
    options.ExcludedPaths.Add("/health");
    options.CollectMemoryMetrics = true;
    options.MetricsInterval = TimeSpan.FromSeconds(5);
});
1
2
3
4
5
6
7
8
9
10
11

应用场景: 监控生产环境中的应用性能,实时检测性能问题,支持DevOps和SRE实践。适用于需要高可用性和性能的关键业务应用。

市场集成: 与Prometheus、Grafana、Application Insights或Datadog等监控系统集成,创建可视化仪表板和报警系统。

# 8. 智能机器人检测中间件

用途: 检测和阻止恶意机器人和网络爬虫,保护网站内容和API。

实现:

public class BotDetectionOptions
{
    public bool BlockBadBots { get; set; } = true;
    public bool AllowGoodBots { get; set; } = true;
    public bool RequireReCaptchaForSuspiciousBots { get; set; } = true;
    public int MaxRequestsPerMinute { get; set; } = 60;
    public int MaxConcurrentRequests { get; set; } = 5;
    public string ReCaptchaSiteKey { get; set; }
    public string ReCaptchaSecretKey { get; set; }
    public TimeSpan ClientHistoryRetention { get; set; } = TimeSpan.FromHours(24);
    public List<string> WhitelistedIps { get; set; } = new List<string>();
    public List<string> WhitelistedUserAgents { get; set; } = new List<string>();
    public List<string> BlacklistedUserAgents { get; set; } = new List<string>();
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public bool EnablePatternDetection { get; set; } = true;
}

public class BotDetectionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly BotDetectionOptions _options;
    private readonly ILogger<BotDetectionMiddleware> _logger;
    private readonly IMemoryCache _cache;
    private readonly HttpClient _httpClient;
    
    // 常见的好机器人用户代理
    private readonly Dictionary<string, string> _knownGoodBots = new Dictionary<string, string>
    {
        { "Googlebot", "Google" },
        { "Bingbot", "Microsoft" },
        { "Slurp", "Yahoo" },
        { "DuckDuckBot", "DuckDuckGo" },
        { "Baiduspider", "Baidu" },
        { "YandexBot", "Yandex" },
        { "facebookexternalhit", "Facebook" }
    };
    
    // 可疑机器人标记
    private readonly List<string> _suspiciousBotMarkers = new List<string>
    {
        "crawler", "spider", "bot", "scraper", "http", "java", "python", "perl",
        "headless", "phantom", "selenium", "automation", "curl", "wget", "fetch"
    };
    
    // 常见的坏机器人用户代理
    private readonly List<string> _knownBadBots = new List<string>
    {
        "ahrefsbot", "semrushbot", "mj12bot", "dotbot", "blexbot", "acunetix", "nessus",
        "sqlmap", "nikto", "nmap", "xenu", "dirbuster", "netcraft", "firebug"
    };

    public BotDetectionMiddleware(
        RequestDelegate next,
        BotDetectionOptions options,
        ILogger<BotDetectionMiddleware> logger,
        IMemoryCache cache)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _cache = cache;
        _httpClient = new HttpClient();
        
        // 添加用户提供的黑名单用户代理
        foreach (var ua in _options.BlacklistedUserAgents)
        {
            if (!_knownBadBots.Contains(ua.ToLowerInvariant()))
            {
                _knownBadBots.Add(ua.ToLowerInvariant());
            }
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否需要跳过检测
        if (ShouldSkipDetection(context))
        {
            await _next(context);
            return;
        }
        
        // 获取客户端信息
        var clientIp = GetClientIp(context);
        var userAgent = context.Request.Headers["User-Agent"].ToString();
        
        // 检查IP白名单
        if (_options.WhitelistedIps.Contains(clientIp))
        {
            await _next(context);
            return;
        }
        
        // 检查用户代理白名单
        if (_options.WhitelistedUserAgents.Any(ua => userAgent.Contains(ua, StringComparison.OrdinalIgnoreCase)))
        {
            await _next(context);
            return;
        }
        
        // 机器人分类
        var botType = CategorizeBotType(userAgent);
        
        switch (botType)
        {
            case BotType.Good:
                if (_options.AllowGoodBots)
                {
                    _logger.LogInformation("Allowed good bot: {UserAgent} from {IP}", userAgent, clientIp);
                    await _next(context);
                    return;
                }
                break;
                
            case BotType.Bad:
                if (_options.BlockBadBots)
                {
                    _logger.LogWarning("Blocked bad bot: {UserAgent} from {IP}", userAgent, clientIp);
                    context.Response.StatusCode = 403; // Forbidden
                    await context.Response.WriteAsync("Access denied");
                    return;
                }
                break;
        }
        
        // 获取客户端历史
        var clientHistory = GetClientHistory(clientIp, userAgent);
        
        // 更新客户端历史
        UpdateClientHistory(clientHistory);
        
        // 检测可疑行为
        var suspiciousLevel = DetectSuspiciousBehavior(clientHistory);
        
        if (suspiciousLevel >= SuspiciousLevel.High)
        {
            _logger.LogWarning("Suspicious client detected: {UserAgent} from {IP}, Level: {Level}", 
                userAgent, clientIp, suspiciousLevel);
            
            if (_options.RequireReCaptchaForSuspiciousBots && !string.IsNullOrEmpty(_options.ReCaptchaSiteKey))
            {
                await RequireReCaptcha(context, clientHistory);
                return;
            }
            
            // 如果没有配置reCAPTCHA,返回403
            if (suspiciousLevel >= SuspiciousLevel.VeryHigh)
            {
                context.Response.StatusCode = 403; // Forbidden
                await context.Response.WriteAsync("Access denied due to suspicious behavior");
                return;
            }
        }
        
        // 记录此次请求
        clientHistory.Requests.Add(new ClientRequest
        {
            Timestamp = DateTime.UtcNow,
            Path = context.Request.Path,
            Method = context.Request.Method
        });
        
        await _next(context);
    }

    private bool ShouldSkipDetection(HttpContext context)
    {
        // 检查排除路径
        if (_options.ExcludedPaths.Any(p => 
            context.Request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }
        
        // 如果是对reCAPTCHA验证的回调,跳过检测
        if (context.Request.Path.Value.EndsWith("/recaptcha-callback", StringComparison.OrdinalIgnoreCase) &&
            context.Request.Method == "POST")
        {
            return true;
        }
        
        return false;
    }

    private string GetClientIp(HttpContext context)
    {
        // 尝试获取X-Forwarded-For头
        string ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        
        // 如果没有X-Forwarded-For头,使用远程IP
        if (string.IsNullOrEmpty(ip))
        {
            ip = context.Connection.RemoteIpAddress?.ToString();
        }
        else
        {
            // X-Forwarded-For可能包含多个IP,第一个是客户端IP
            var ips = ip.Split(',', StringSplitOptions.RemoveEmptyEntries);
            ip = ips[0].Trim();
        }
        
        return ip;
    }

    private BotType CategorizeBotType(string userAgent)
    {
        if (string.IsNullOrEmpty(userAgent))
        {
            return BotType.Suspicious; // 没有用户代理是可疑的
        }
        
        userAgent = userAgent.ToLowerInvariant();
        
        // 检查已知的好机器人
        foreach (var bot in _knownGoodBots)
        {
            if (userAgent.Contains(bot.Key.ToLowerInvariant()))
            {
                // 简单的验证方法,在实际应用中,可能需要使用更复杂的方法验证
                // 例如,反向DNS查询以确认是否真的是声称的机器人
                return BotType.Good;
            }
        }
        
        // 检查已知的坏机器人
        foreach (var bot in _knownBadBots)
        {
            if (userAgent.Contains(bot))
            {
                return BotType.Bad;
            }
        }
        
        // 检查可疑标记
        if (_options.EnablePatternDetection)
        {
            foreach (var marker in _suspiciousBotMarkers)
            {
                if (userAgent.Contains(marker))
                {
                    return BotType.Suspicious;
                }
            }
        }
        
        return BotType.Unknown;
    }

    private ClientHistory GetClientHistory(string ip, string userAgent)
    {
        // 使用IP和用户代理的组合作为缓存键
        var cacheKey = $"bot_detection:{ip}:{userAgent.GetHashCode()}";
        
        if (!_cache.TryGetValue(cacheKey, out ClientHistory history))
        {
            history = new ClientHistory
            {
                IP = ip,
                UserAgent = userAgent,
                FirstSeen = DateTime.UtcNow,
                Requests = new List<ClientRequest>(),
                ChallengesPassed = 0,
                ChallengesFailed = 0
            };
            
            // 使用滑动过期缓存客户端历史
            _cache.Set(cacheKey, history, new MemoryCacheEntryOptions
            {
                SlidingExpiration = _options.ClientHistoryRetention
            });
        }
        
        return history;
    }

    private void UpdateClientHistory(ClientHistory history)
    {
        // 移除旧请求
        var cutoffTime = DateTime.UtcNow.AddMinutes(-1);
        history.Requests.RemoveAll(r => r.Timestamp < cutoffTime);
    }

    private SuspiciousLevel DetectSuspiciousBehavior(ClientHistory history)
    {
        var suspiciousScore = 0;
        
        // 检查请求率
        var requestsLastMinute = history.Requests.Count;
        if (requestsLastMinute > _options.MaxRequestsPerMinute)
        {
            suspiciousScore += 30;
        }
        else if (requestsLastMinute > _options.MaxRequestsPerMinute * 0.7)
        {
            suspiciousScore += 15;
        }
        
        // 检查请求模式
        if (history.Requests.Count >= 5)
        {
            // 检查是否有固定的请求间隔
            var intervalPattern = DetectFixedInterval(history.Requests);
            if (intervalPattern)
            {
                suspiciousScore += 20;
            }
            
            // 检查是否只访问特定类型的资源
            var resourcePattern = DetectResourcePattern(history.Requests);
            if (resourcePattern)
            {
                suspiciousScore += 10;
            }
        }
        
        // 检查失败的挑战
        if (history.ChallengesFailed > 0)
        {
            suspiciousScore += 25 * history.ChallengesFailed;
        }
        
        // 根据分数确定可疑级别
        if (suspiciousScore >= 70)
        {
            return SuspiciousLevel.VeryHigh;
        }
        else if (suspiciousScore >= 40)
        {
            return SuspiciousLevel.High;
        }
        else if (suspiciousScore >= 20)
        {
            return SuspiciousLevel.Medium;
        }
        else if (suspiciousScore > 0)
        {
            return SuspiciousLevel.Low;
        }
        
        return SuspiciousLevel.None;
    }

    private bool DetectFixedInterval(List<ClientRequest> requests)
    {
        if (requests.Count < 5)
            return false;
            
        var intervals = new List<double>();
        
        // 计算请求之间的时间间隔
        for (int i = 1; i < requests.Count; i++)
        {
            intervals.Add((requests[i].Timestamp - requests[i-1].Timestamp).TotalMilliseconds);
        }
        
        // 计算间隔的标准差
        var avg = intervals.Average();
        var variance = intervals.Sum(i => Math.Pow(i - avg, 2)) / intervals.Count;
        var stdDev = Math.Sqrt(variance);
        
        // 如果标准差较小,则说明间隔较为固定
        return stdDev / avg < 0.2; // 20%的变异系数阈值
    }

    private bool DetectResourcePattern(List<ClientRequest> requests)
    {
        // 检查访问的资源类型是否单一
        var extensions = requests
            .Select(r => Path.GetExtension(r.Path))
            .Where(ext => !string.IsNullOrEmpty(ext))
            .GroupBy(ext => ext)
            .ToDictionary(g => g.Key, g => g.Count());
            
        // 如果90%以上的请求都是访问同一类型的资源,则可能是在爬取特定内容
        if (extensions.Any())
        {
            var mostCommonExt = extensions.OrderByDescending(kv => kv.Value).First();
            return (double)mostCommonExt.Value / requests.Count > 0.9;
        }
        
        return false;
    }

    private async Task RequireReCaptcha(HttpContext context, ClientHistory history)
    {
        // 检查请求是否是对reCAPTCHA验证的回调
        if (context.Request.Method == "POST" && 
            context.Request.HasFormContentType && 
            context.Request.Form.ContainsKey("g-recaptcha-response"))
        {
            var recaptchaResponse = context.Request.Form["g-recaptcha-response"].ToString();
            var redirectUrl = context.Request.Form["redirect"].ToString();
            
            // 验证reCAPTCHA响应
            var isValid = await VerifyReCaptchaAsync(recaptchaResponse, GetClientIp(context));
            
            if (isValid)
            {
                // 验证成功,记录通过
                history.ChallengesPassed++;
                
                // 如果提供了重定向URL,重定向到该URL
                if (!string.IsNullOrEmpty(redirectUrl))
                {
                    context.Response.Redirect(redirectUrl);
                }
                else
                {
                    // 否则返回成功消息
                    context.Response.StatusCode = 200;
                    await context.Response.WriteAsync("Verification successful. You may now continue browsing.");
                }
            }
            else
            {
                // 验证失败,记录失败
                history.ChallengesFailed++;
                
                // 返回验证页面,显示错误消息
                await SendReCaptchaPage(context, "reCAPTCHA verification failed. Please try again.", redirectUrl);
            }
            
            return;
        }
        
        // 发送包含reCAPTCHA的页面
        var originalUrl = context.Request.Path + context.Request.QueryString;
        await SendReCaptchaPage(context, null, originalUrl);
    }

    private async Task<bool> VerifyReCaptchaAsync(string recaptchaResponse, string remoteIp)
    {
        if (string.IsNullOrEmpty(recaptchaResponse))
            return false;
            
        var verifyUrl = "https://www.google.com/recaptcha/api/siteverify";
        
        var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["secret"] = _options.ReCaptchaSecretKey,
            ["response"] = recaptchaResponse,
            ["remoteip"] = remoteIp
        });
        
        try
        {
            var response = await _httpClient.PostAsync(verifyUrl, content);
            var responseJson = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<ReCaptchaVerifyResponse>(responseJson);
            
            return result?.Success == true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error verifying reCAPTCHA response");
            return false;
        }
    }

    private async Task SendReCaptchaPage(HttpContext context, string errorMessage, string redirectUrl)
    {
        context.Response.StatusCode = 200;
        context.Response.ContentType = "text/html";
        
        var html = $@"
<!DOCTYPE html>
<html>
<head>
    <title>Verify you are human</title>
    <script src='https://www.google.com/recaptcha/api.js' async defer></script>
    <style>
        body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }}
        .container {{ max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }}
        .error {{ color: red; margin: 15px 0; }}
    </style>
</head>
<body>
    <div class='container'>
        <h2>Please verify you are human</h2>
        <p>For security reasons, we need to verify that you are not a robot.</p>
        
        {(errorMessage != null ? $"<p class='error'>{errorMessage}</p>" : "")}
        
        <form action='/recaptcha-callback' method='post'>
            <div class='g-recaptcha' data-sitekey='{_options.ReCaptchaSiteKey}'></div>
            <input type='hidden' name='redirect' value='{redirectUrl}'>
            <br/>
            <button type='submit'>Submit</button>
        </form>
    </div>
</body>
</html>";
        
        await context.Response.WriteAsync(html);
    }

    private enum BotType
    {
        Unknown,
        Good,
        Bad,
        Suspicious
    }

    private enum SuspiciousLevel
    {
        None = 0,
        Low = 1,
        Medium = 2,
        High = 3,
        VeryHigh = 4
    }

    private class ClientHistory
    {
        public string IP { get; set; }
        public string UserAgent { get; set; }
        public DateTime FirstSeen { get; set; }
        public List<ClientRequest> Requests { get; set; }
        public int ChallengesPassed { get; set; }
        public int ChallengesFailed { get; set; }
    }

    private class ClientRequest
    {
        public DateTime Timestamp { get; set; }
        public string Path { get; set; }
        public string Method { get; set; }
    }

    private class ReCaptchaVerifyResponse
    {
        [JsonPropertyName("success")]
        public bool Success { get; set; }
        
        [JsonPropertyName("error-codes")]
        public List<string> ErrorCodes { get; set; }
    }
}

public static class BotDetectionMiddlewareExtensions
{
    public static IApplicationBuilder UseBotDetection(
        this IApplicationBuilder builder,
        Action<BotDetectionOptions> configureOptions = null)
    {
        var options = new BotDetectionOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<BotDetectionMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552

注册方式:

// 在Startup.ConfigureServices中
services.AddMemoryCache();

// 在Startup.Configure中
app.UseBotDetection(options => {
    options.BlockBadBots = true;
    options.AllowGoodBots = true;
    options.RequireReCaptchaForSuspiciousBots = true;
    options.ReCaptchaSiteKey = Configuration["ReCaptcha:SiteKey"];
    options.ReCaptchaSecretKey = Configuration["ReCaptcha:SecretKey"];
    options.MaxRequestsPerMinute = 60;
    
    options.WhitelistedIps.Add("127.0.0.1");
    options.WhitelistedUserAgents.Add("PostmanRuntime");
    
    options.BlacklistedUserAgents.Add("DataForSeoBot");
    options.BlacklistedUserAgents.Add("SemrushBot");
    
    options.ExcludedPaths.Add("/images");
    options.ExcludedPaths.Add("/css");
    options.ExcludedPaths.Add("/js");
    options.ExcludedPaths.Add("/recaptcha-callback");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

应用场景: 保护网站内容不被爬虫抓取,防止API滥用,减少DDoS攻击影响。适用于内容网站、电子商务平台和具有高价值数据的应用。

市场集成: 与Google reCAPTCHA、Cloudflare、PerimeterX Bot Defender等第三方服务集成,增强机器人检测能力。

# 9. 服务降级与熔断中间件

用途: 在系统压力大或依赖服务不可用时,自动降级服务或断开依赖调用,保护系统稳定性。

实现:

public class CircuitBreakerOptions
{
    public int FailureThreshold { get; set; } = 5;
    public TimeSpan BreakDuration { get; set; } = TimeSpan.FromSeconds(30);
    public TimeSpan HealthCheckInterval { get; set; } = TimeSpan.FromSeconds(5);
    public int HealthCheckSampleSize { get; set; } = 3;
    public double HealthCheckSuccessThreshold { get; set; } = 0.5;
    public List<string> MonitoredPaths { get; set; } = new List<string>();
    public Dictionary<string, PathCircuitOptions> PathOptions { get; set; } = new Dictionary<string, PathCircuitOptions>();
    public bool EnableFallbacks { get; set; } = true;
    public TimeSpan FallbackCacheDuration { get; set; } = TimeSpan.FromMinutes(10);
    public Func<HttpContext, Exception, Task<bool>> OnCircuitBroken { get; set; }
}

public class PathCircuitOptions
{
    public int FailureThreshold { get; set; }
    public TimeSpan BreakDuration { get; set; }
    public bool EnableFallback { get; set; } = true;
    public string FallbackPath { get; set; }
}

public class CircuitBreakerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly CircuitBreakerOptions _options;
    private readonly ILogger<CircuitBreakerMiddleware> _logger;
    private readonly IMemoryCache _cache;
    private readonly ConcurrentDictionary<string, CircuitBreaker> _circuitBreakers = new ConcurrentDictionary<string, CircuitBreaker>();

    public CircuitBreakerMiddleware(
        RequestDelegate next,
        CircuitBreakerOptions options,
        ILogger<CircuitBreakerMiddleware> logger,
        IMemoryCache cache)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _cache = cache;
        
        // 为每个配置的路径创建断路器
        foreach (var path in _options.MonitoredPaths)
        {
            CreateCircuitBreaker(path);
        }
        
        foreach (var pathOption in _options.PathOptions)
        {
            CreateCircuitBreaker(pathOption.Key, pathOption.Value);
        }
    }

    private CircuitBreaker CreateCircuitBreaker(string path, PathCircuitOptions pathOptions = null)
    {
        var options = pathOptions ?? new PathCircuitOptions
        {
            FailureThreshold = _options.FailureThreshold,
            BreakDuration = _options.BreakDuration,
            EnableFallback = _options.EnableFallbacks
        };
        
        var breaker = new CircuitBreaker(
            path,
            options.FailureThreshold,
            options.BreakDuration,
            _options.HealthCheckInterval,
            _options.HealthCheckSampleSize,
            _options.HealthCheckSuccessThreshold,
            options.FallbackPath,
            _logger);
            
        _circuitBreakers[path] = breaker;
        return breaker;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;
        
        // 找到匹配的断路器
        CircuitBreaker circuitBreaker = null;
        string matchedPath = null;
        
        foreach (var breaker in _circuitBreakers)
        {
            if (path.StartsWith(breaker.Key, StringComparison.OrdinalIgnoreCase))
            {
                if (circuitBreaker == null || breaker.Key.Length > (matchedPath?.Length ?? 0))
                {
                    circuitBreaker = breaker.Value;
                    matchedPath = breaker.Key;
                }
            }
        }
        
        // 如果没有找到匹配的断路器,直接处理请求
        if (circuitBreaker == null)
        {
            await _next(context);
            return;
        }
        
        // 检查断路器状态
        var state = circuitBreaker.GetState();
        
        if (state == CircuitState.Open)
        {
            _logger.LogWarning("Circuit is open for {Path}. Request rejected.", matchedPath);
            
            // 尝试提供回退响应
            if (_options.EnableFallbacks && await TryProvideFallback(context, circuitBreaker, null))
            {
                return;
            }
            
            // 允许调用者提供自定义断路处理
            if (_options.OnCircuitBroken != null && await _options.OnCircuitBroken(context, null))
            {
                return;
            }
            
            // 默认行为:返回服务不可用
            context.Response.StatusCode = 503; // Service Unavailable
            context.Response.Headers.Add("Retry-After", ((int)_options.BreakDuration.TotalSeconds).ToString());
            await context.Response.WriteAsync("Service temporarily unavailable. Please try again later.");
            return;
        }
        
        // 捕获原始响应体
        var originalBodyStream = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            // 如果是半开状态,这将是一个健康检查请求
            bool isHealthCheck = state == CircuitState.HalfOpen;
            
            if (isHealthCheck)
            {
                _logger.LogInformation("Circuit is half-open for {Path}. Performing health check.", matchedPath);
            }
            
            // 记录开始时间
            var startTime = DateTime.UtcNow;
            
            await _next(context);
            
            // 记录响应时间
            var responseTime = DateTime.UtcNow - startTime;
            
            // 检查响应是否成功
            var isSuccess = context.Response.StatusCode < 500;
            
            if (isSuccess)
            {
                circuitBreaker.RecordSuccess();
                
                if (isHealthCheck)
                {
                    _logger.LogInformation("Health check succeeded for {Path}. Circuit might close.", matchedPath);
                }
                
                // 缓存成功的响应以备将来断路时使用
                if (_options.EnableFallbacks)
                {
                    await CacheSuccessfulResponse(context, matchedPath, responseBodyStream);
                }
            }
            else
            {
                circuitBreaker.RecordFailure();
                
                if (isHealthCheck)
                {
                    _logger.LogWarning("Health check failed for {Path}. Circuit remains open.", matchedPath);
                }
            }
            
            // 将响应体复制回原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBodyStream);
        }
        catch (Exception ex)
        {
            // 记录失败
            circuitBreaker.RecordFailure();
            
            _logger.LogError(ex, "Circuit detected failure for {Path}", matchedPath);
            
            // 尝试提供回退响应
            if (_options.EnableFallbacks && await TryProvideFallback(context, circuitBreaker, ex))
            {
                context.Response.Body = originalBodyStream;
                return;
            }
            
            // 允许调用者提供自定义断路处理
            if (_options.OnCircuitBroken != null && await _options.OnCircuitBroken(context, ex))
            {
                context.Response.Body = originalBodyStream;
                return;
            }
            
            // 恢复原始响应流并重新抛出异常
            context.Response.Body = originalBodyStream;
            throw;
        }
    }

    private async Task CacheSuccessfulResponse(HttpContext context, string path, MemoryStream responseBodyStream)
    {
        // 只缓存GET请求的成功响应
        if (context.Request.Method != "GET" || context.Response.StatusCode != 200)
        {
            return;
        }
        
        // 不缓存太大的响应
        if (responseBodyStream.Length > 1024 * 1024) // 1MB
        {
            return;
        }
        
        // 构建缓存键
        var cacheKey = $"fallback:{path}:{context.Request.Path}:{context.Request.QueryString}";
        
        // 保存响应信息
        responseBodyStream.Position = 0;
        var responseBody = await new StreamReader(responseBodyStream).ReadToEndAsync();
        
        var cachedResponse = new CachedResponse
        {
            Body = responseBody,
            ContentType = context.Response.ContentType,
            Headers = context.Response.Headers
                .Where(h => !h.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase))
                .ToDictionary(h => h.Key, h => h.Value.ToString())
        };
        
        // 使用滑动过期缓存响应
        _cache.Set(cacheKey, cachedResponse, new MemoryCacheEntryOptions
        {
            SlidingExpiration = _options.FallbackCacheDuration
        });
    }

    private async Task<bool> TryProvideFallback(HttpContext context, CircuitBreaker breaker, Exception exception)
    {
        // 如果有指定回退路径,重定向到该路径
        if (!string.IsNullOrEmpty(breaker.FallbackPath))
        {
            var fallbackPath = breaker.FallbackPath;
            
            // 检查是否包含占位符
            if (fallbackPath.Contains("{path}"))
            {
                fallbackPath = fallbackPath.Replace("{path}", context.Request.Path.Value.TrimStart('/'));
            }
            
            _logger.LogInformation("Redirecting to fallback path: {FallbackPath}", fallbackPath);
            context.Response.Redirect(fallbackPath);
            return true;
        }
        
        // 尝试从缓存中获取之前的成功响应
        var cacheKey = $"fallback:{breaker.Path}:{context.Request.Path}:{context.Request.QueryString}";
        
        if (_cache.TryGetValue(cacheKey, out CachedResponse cachedResponse))
        {
            _logger.LogInformation("Serving cached fallback response for {Path}", context.Request.Path);
            
            // 设置响应头
            context.Response.StatusCode = 200;
            context.Response.ContentType = cachedResponse.ContentType;
            
            foreach (var header in cachedResponse.Headers)
            {
                context.Response.Headers[header.Key] = header.Value;
            }
            
            // 添加标记,表明这是一个缓存的回退响应
            context.Response.Headers["X-Fallback-Response"] = "true";
            
            // 写入缓存的响应体
            await context.Response.WriteAsync(cachedResponse.Body);
            return true;
        }
        
        return false;
    }

    private enum CircuitState
    {
        Closed,
        Open,
        HalfOpen
    }

    private class CircuitBreaker
    {
        private readonly string _path;
        private readonly int _failureThreshold;
        private readonly TimeSpan _breakDuration;
        private readonly TimeSpan _healthCheckInterval;
        private readonly int _healthCheckSampleSize;
        private readonly double _healthCheckSuccessThreshold;
        private readonly string _fallbackPath;
        private readonly ILogger _logger;
        
        private int _failureCount;
        private CircuitState _state = CircuitState.Closed;
        private DateTime _openedAt;
        private DateTime _lastHealthCheck;
        private readonly List<bool> _healthCheckResults = new List<bool>();
        private readonly object _stateLock = new object();
        
        public string Path => _path;
        public string FallbackPath => _fallbackPath;
        
        public CircuitBreaker(
            string path,
            int failureThreshold,
            TimeSpan breakDuration,
            TimeSpan healthCheckInterval,
            int healthCheckSampleSize,
            double healthCheckSuccessThreshold,
            string fallbackPath,
            ILogger logger)
        {
            _path = path;
            _failureThreshold = failureThreshold;
            _breakDuration = breakDuration;
            _healthCheckInterval = healthCheckInterval;
            _healthCheckSampleSize = healthCheckSampleSize;
            _healthCheckSuccessThreshold = healthCheckSuccessThreshold;
            _fallbackPath = fallbackPath;
            _logger = logger;
        }
        
        public CircuitState GetState()
        {
            lock (_stateLock)
            {
                if (_state == CircuitState.Open)
                {
                    // 检查是否应该进入半开状态
                    if (DateTime.UtcNow - _openedAt >= _breakDuration &&
                        DateTime.UtcNow - _lastHealthCheck >= _healthCheckInterval)
                    {
                        _state = CircuitState.HalfOpen;
                        _lastHealthCheck = DateTime.UtcNow;
                        _logger.LogInformation("Circuit for {Path} transitioning from Open to HalfOpen", _path);
                    }
                }
                
                return _state;
            }
        }
        
        public void RecordSuccess()
        {
            lock (_stateLock)
            {
                if (_state == CircuitState.Closed)
                {
                    // 重置失败计数
                    _failureCount = 0;
                }
                else if (_state == CircuitState.HalfOpen)
                {
                    // 在半开状态下记录健康检查成功
                    _healthCheckResults.Add(true);
                    
                    // 检查是否有足够的健康检查样本
                    if (_healthCheckResults.Count >= _healthCheckSampleSize)
                    {
                        var successRate = (double)_healthCheckResults.Count(r => r) / _healthCheckResults.Count;
                        
                        if (successRate >= _healthCheckSuccessThreshold)
                        {
                            // 关闭断路器
                            _state = CircuitState.Closed;
                            _failureCount = 0;
                            _healthCheckResults.Clear();
                            _logger.LogInformation("Circuit for {Path} closed after successful health checks", _path);
                        }
                        else
                        {
                            // 保持断路器打开
                            _state = CircuitState.Open;
                            _openedAt = DateTime.UtcNow;
                            _healthCheckResults.Clear();
                            _logger.LogWarning("Circuit for {Path} remains open after failed health checks", _path);
                        }
                    }
                }
            }
        }
        
        public void RecordFailure()
        {
            lock (_stateLock)
            {
                if (_state == CircuitState.Closed)
                {
                    _failureCount++;
                    
                    if (_failureCount >= _failureThreshold)
                    {
                        // 打开断路器
                        _state = CircuitState.Open;
                        _openedAt = DateTime.UtcNow;
                        _logger.LogWarning("Circuit for {Path} opened after {FailureCount} consecutive failures", 
                            _path, _failureCount);
                    }
                }
                else if (_state == CircuitState.HalfOpen)
                {
                    // 健康检查失败,保持断路器打开
                    _healthCheckResults.Add(false);
                    
                    // 如果有足够的样本或都是失败的,则立即重新打开断路器
                    if (_healthCheckResults.Count >= _healthCheckSampleSize || 
                        _healthCheckResults.All(r => !r))
                    {
                        _state = CircuitState.Open;
                        _openedAt = DateTime.UtcNow;
                        _healthCheckResults.Clear();
                        _logger.LogWarning("Circuit for {Path} reopened after failed health checks", _path);
                    }
                }
            }
        }
    }

    private class CachedResponse
    {
        public string Body { get; set; }
        public string ContentType { get; set; }
        public Dictionary<string, string> Headers { get; set; }
    }
}

public static class CircuitBreakerMiddlewareExtensions
{
    public static IApplicationBuilder UseCircuitBreaker(
        this IApplicationBuilder builder,
        Action<CircuitBreakerOptions> configureOptions = null)
    {
        var options = new CircuitBreakerOptions();
        configureOptions?.Invoke(options);
        
        return builder.UseMiddleware<CircuitBreakerMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457

注册方式:

// 在Startup.ConfigureServices中
services.AddMemoryCache();

// 在Startup.Configure中
app.UseCircuitBreaker(options => {
    // 监控特定路径
    options.MonitoredPaths.Add("/api/external");
    options.MonitoredPaths.Add("/api/payments");
    
    // 为特定路径配置选项
    options.PathOptions["/api/payments"] = new PathCircuitOptions {
        FailureThreshold = 3,
        BreakDuration = TimeSpan.FromMinutes(1),
        FallbackPath = "/api/payments-fallback"
    };
    
    // 启用回退机制
    options.EnableFallbacks = true;
    options.FallbackCacheDuration = TimeSpan.FromMinutes(5);
    
    // 断路时的自定义处理
    options.OnCircuitBroken = async (context, exception) => {
        if (context.Request.Path.StartsWithSegments("/api/critical"))
        {
            // 关键API的自定义处理
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("Critical service unavailable. Please contact support.");
            return true;
        }
        return false;
    };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

应用场景: 保护系统免受级联故障影响,在微服务架构中特别有用。例如,当支付服务不可用时,电子商务网站可以降级为"稍后付款"选项,而不是完全失败。

市场集成: 与Polly和Steeltoe集成,实现更复杂的故障处理策略;与健康监控系统集成,提供服务健康状态可视化。

# 10. 分布式追踪中间件

用途: 跟踪请求在分布式系统中的传播路径,帮助诊断性能问题和错误。

实现:

public class DistributedTracingOptions
{
    public string ServiceName { get; set; } = Assembly.GetEntryAssembly().GetName().Name;
    public bool PropagateContext { get; set; } = true;
    public bool LogTraceEvents { get; set; } = true;
    public bool IncludeRequestBody { get; set; } = false;
    public bool IncludeResponseBody { get; set; } = false;
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public int MaxBodySizeToCapture { get; set; } = 4096; // 4KB
    public List<string> HeadersToCapture { get; set; } = new List<string>
    {
        "User-Agent",
        "Referer",
        "Accept",
        "Content-Type"
    };
    public TracingExporter Exporter { get; set; } = TracingExporter.Console;
    public string JaegerEndpoint { get; set; } = "http://localhost:14268/api/traces";
    public string ZipkinEndpoint { get; set; } = "http://localhost:9411/api/v2/spans";
}

public enum TracingExporter
{
    Console,
    Jaeger,
    Zipkin,
    ApplicationInsights
}

public class DistributedTracingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly DistributedTracingOptions _options;
    private readonly ILogger<DistributedTracingMiddleware> _logger;
    private readonly ActivitySource _activitySource;
    private readonly IHttpClientFactory _httpClientFactory;

    public DistributedTracingMiddleware(
        RequestDelegate next,
        DistributedTracingOptions options,
        ILogger<DistributedTracingMiddleware> logger,
        IHttpClientFactory httpClientFactory)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _httpClientFactory = httpClientFactory;
        
        // 创建活动源
        _activitySource = new ActivitySource(_options.ServiceName);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该跳过追踪
        if (ShouldExcludeRequest(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 提取传播的上下文
        var parentContext = ExtractPropagationContext(context.Request);
        
        // 创建活动(span)
        using var activity = CreateActivity(context.Request, parentContext);
        
        if (activity == null)
        {
            await _next(context);
            return;
        }
        
        // 捕获请求体(如果配置了)
        string requestBody = null;
        if (_options.IncludeRequestBody && IsContentTypeCaptureable(context.Request.ContentType))
        {
            requestBody = await CaptureRequestBody(context.Request);
            if (!string.IsNullOrEmpty(requestBody))
            {
                activity.SetTag("http.request.body", requestBody);
            }
        }
        
        // 捕获原始响应流
        var originalBodyStream = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        Exception exception = null;
        
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            exception = ex;
            throw;
        }
        finally
        {
            // 记录响应信息
            activity.SetTag("http.status_code", context.Response.StatusCode);
            activity.SetTag("http.response.headers.content_type", context.Response.ContentType);
            
            // 捕获响应体(如果配置了)
            if (_options.IncludeResponseBody && IsContentTypeCaptureable(context.Response.ContentType))
            {
                responseBodyStream.Position = 0;
                var responseBody = await new StreamReader(responseBodyStream).ReadToEndAsync();
                
                if (!string.IsNullOrEmpty(responseBody) && responseBody.Length <= _options.MaxBodySizeToCapture)
                {
                    activity.SetTag("http.response.body", responseBody);
                }
                
                // 重置流位置,以便复制回原始流
                responseBodyStream.Position = 0;
            }
            
            // 复制响应流回原始流
            await responseBodyStream.CopyToAsync(originalBodyStream);
            
            // 记录异常(如果有)
            if (exception != null)
            {
                activity.SetTag("error", true);
                activity.SetTag("error.type", exception.GetType().FullName);
                activity.SetTag("error.message", exception.Message);
                activity.SetTag("error.stack", exception.StackTrace);
            }
            
            // 停止活动并导出跟踪
            activity.Stop();
            
            if (_options.LogTraceEvents)
            {
                var logLevel = exception != null ? LogLevel.Error : LogLevel.Information;
                _logger.Log(logLevel, "Trace completed: {TraceId}, Parent: {ParentId}, Duration: {Duration}ms, Status: {StatusCode}",
                    activity.TraceId, activity.ParentId, activity.Duration.TotalMilliseconds, context.Response.StatusCode);
            }
            
            // 导出跟踪(异步)
            _ = ExportTraceAsync(activity);
        }
    }

    private bool ShouldExcludeRequest(HttpRequest request)
    {
        return _options.ExcludedPaths.Any(p => 
            request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase));
    }

    private ActivityContext ExtractPropagationContext(HttpRequest request)
    {
        if (!_options.PropagateContext)
        {
            return default;
        }
        
        // 检查W3C跟踪上下文头部
        var traceParent = request.Headers["traceparent"].FirstOrDefault();
        var traceState = request.Headers["tracestate"].FirstOrDefault();
        
        if (string.IsNullOrEmpty(traceParent))
        {
            return default;
        }
        
        // 解析traceparent头部格式:00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
        // 版本-跟踪ID-父span ID-跟踪标志
        var parts = traceParent.Split('-');
        if (parts.Length != 4)
        {
            return default;
        }
        
        try
        {
            var traceIdString = parts[1];
            var parentIdString = parts[2];
            var flagsString = parts[3];
            
            if (ActivityTraceId.TryParse(traceIdString, out var traceId) &&
                ActivitySpanId.TryParse(parentIdString, out var spanId))
            {
                var flags = byte.Parse(flagsString, NumberStyles.HexNumber);
                var traceFlags = (ActivityTraceFlags)(flags & 0x01); // 提取采样位
                
                return new ActivityContext(traceId, spanId, traceFlags, traceState);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error parsing trace context headers");
        }
        
        return default;
    }

    private Activity CreateActivity(HttpRequest request, ActivityContext parentContext)
    {
        var activityName = $"{request.Method} {request.Path}";
        
        Activity activity;
        if (parentContext != default)
        {
            activity = _activitySource.StartActivity(activityName, ActivityKind.Server, parentContext);
        }
        else
        {
            activity = _activitySource.StartActivity(activityName, ActivityKind.Server);
        }
        
        if (activity == null)
        {
            return null;
        }
        
        // 添加基本请求信息
        activity.SetTag("http.method", request.Method);
        activity.SetTag("http.url", $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}");
        activity.SetTag("http.host", request.Host.ToString());
        activity.SetTag("http.path", request.Path.ToString());
        activity.SetTag("http.scheme", request.Scheme);
        
        // 添加请求头信息
        foreach (var headerName in _options.HeadersToCapture)
        {
            if (request.Headers.TryGetValue(headerName, out var headerValue))
            {
                activity.SetTag($"http.request.headers.{headerName.ToLowerInvariant()}", headerValue.ToString());
            }
        }
        
        // 添加客户端信息
        activity.SetTag("http.client_ip", GetClientIp(request));
        
        // 如果有用户身份,添加用户信息
        var user = request.HttpContext.User;
        if (user?.Identity?.IsAuthenticated == true)
        {
            activity.SetTag("enduser.id", user.FindFirst(ClaimTypes.NameIdentifier)?.Value);
            activity.SetTag("enduser.role", string.Join(",", user.FindAll(ClaimTypes.Role).Select(c => c.Value)));
        }
        
        return activity;
    }

    private async Task<string> CaptureRequestBody(HttpRequest request)
    {
        if (!request.Body.CanRead || request.ContentLength == 0)
        {
            return null;
        }
        
        // 限制捕获大小
        if (request.ContentLength > _options.MaxBodySizeToCapture)
        {
            return $"[Body too large to capture. Size: {request.ContentLength} bytes]";
        }
        
        // 启用缓冲以便多次读取
        request.EnableBuffering();
        
        using var reader = new StreamReader(
            request.Body,
            encoding: Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            bufferSize: _options.MaxBodySizeToCapture,
            leaveOpen: true);
            
        var body = await reader.ReadToEndAsync();
        
        // 重置流位置,以便后续中间件可以读取
        request.Body.Position = 0;
        
        return body;
    }

    private bool IsContentTypeCaptureable(string contentType)
    {
        if (string.IsNullOrEmpty(contentType))
        {
            return false;
        }
        
        var mediaType = contentType.Split(';')[0].Trim().ToLowerInvariant();
        
        return mediaType.StartsWith("application/json") || 
               mediaType.StartsWith("application/xml") || 
               mediaType.StartsWith("text/");
    }

    private string GetClientIp(HttpRequest request)
    {
        var xForwardedFor = request.Headers["X-Forwarded-For"].FirstOrDefault();
        if (!string.IsNullOrEmpty(xForwardedFor))
        {
            return xForwardedFor.Split(',')[0].Trim();
        }
        
        return request.HttpContext.Connection.RemoteIpAddress?.ToString();
    }

    private async Task ExportTraceAsync(Activity activity)
    {
        switch (_options.Exporter)
        {
            case TracingExporter.Console:
                // 简单地将跟踪信息记录到控制台,已在日志中完成
                break;
                
            case TracingExporter.Jaeger:
                await ExportToJaegerAsync(activity);
                break;
                
            case TracingExporter.Zipkin:
                await ExportToZipkinAsync(activity);
                break;
                
            case TracingExporter.ApplicationInsights:
                // 使用Application Insights的原生活动支持,不需要额外导出
                break;
        }
    }

    private async Task ExportToJaegerAsync(Activity activity)
    {
        try
        {
            // 简化的Jaeger格式转换
            var span = new
            {
                traceId = activity.TraceId.ToHexString(),
                spanId = activity.SpanId.ToHexString(),
                parentSpanId = activity.ParentSpanId.ToHexString(),
                operationName = activity.DisplayName,
                startTime = activity.StartTimeUtc.ToUnixTimeMicroseconds(),
                duration = (long)(activity.Duration.TotalMilliseconds * 1000),
                tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value)
            };
            
            var jaegerSpan = new
            {
                process = new
                {
                    serviceName = _options.ServiceName
                },
                spans = new[] { span }
            };
            
            var content = new StringContent(
                JsonSerializer.Serialize(jaegerSpan),
                Encoding.UTF8,
                "application/json");
                
            using var client = _httpClientFactory.CreateClient("DistributedTracing");
            await client.PostAsync(_options.JaegerEndpoint, content);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting trace to Jaeger");
        }
    }

    private async Task ExportToZipkinAsync(Activity activity)
    {
        try
        {
            // 简化的Zipkin格式转换
            var zipkinSpan = new
            {
                id = activity.SpanId.ToHexString(),
                traceId = activity.TraceId.ToHexString(),
                parentId = activity.ParentSpanId.ToHexString(),
                name = activity.DisplayName,
                timestamp = activity.StartTimeUtc.ToUnixTimeMicroseconds(),
                duration = (long)(activity.Duration.TotalMilliseconds * 1000),
                localEndpoint = new
                {
                    serviceName = _options.ServiceName
                },
                tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value.ToString())
            };
            
            var content = new StringContent(
                JsonSerializer.Serialize(new[] { zipkinSpan }),
                Encoding.UTF8,
                "application/json");
                
            using var client = _httpClientFactory.CreateClient("DistributedTracing");
            await client.PostAsync(_options.ZipkinEndpoint, content);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting trace to Zipkin");
        }
    }
}

public static class DistributedTracingMiddlewareExtensions
{
    public static IServiceCollection AddDistributedTracing(
        this IServiceCollection services,
        Action<DistributedTracingOptions> configureOptions = null)
    {
        var options = new DistributedTracingOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        services.AddHttpClient();
        
        // 注册活动监听器
        services.AddSingleton<ActivityListener>(sp =>
        {
            var listener = new ActivityListener
            {
                ShouldListenTo = source => source.Name == options.ServiceName,
                Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData
            };
            
            ActivitySource.AddActivityListener(listener);
            return listener;
        });
        
        return services;
    }
    
    public static IApplicationBuilder UseDistributedTracing(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<DistributedTracingMiddleware>();
    }
}

public static class DateTimeExtensions
{
    private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
    
    public static long ToUnixTimeMicroseconds(this DateTime dateTime)
    {
        return (long)(dateTime - UnixEpoch.UtcDateTime).TotalMilliseconds * 1000;
    }
    
    public static long ToUnixTimeMicroseconds(this DateTimeOffset dateTime)
    {
        return (long)(dateTime - UnixEpoch).TotalMilliseconds * 1000;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450

注册方式:

// 在Startup.ConfigureServices中
services.AddDistributedTracing(options => {
    options.ServiceName = "OrderService";
    options.PropagateContext = true;
    options.IncludeRequestBody = true;
    options.MaxBodySizeToCapture = 8192; // 8KB
    options.HeadersToCapture.Add("X-Correlation-Id");
    options.ExcludedPaths.Add("/health");
    options.ExcludedPaths.Add("/metrics");
    
    // 配置导出器
    options.Exporter = TracingExporter.Jaeger;
    options.JaegerEndpoint = "http://jaeger:14268/api/traces";
});

// 在Startup.Configure中
app.UseDistributedTracing();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

应用场景: 在微服务架构中追踪请求流,帮助诊断性能问题和服务依赖。特别适用于复杂的分布式系统,如电子商务平台、金融系统和云原生应用。

市场集成: 与OpenTelemetry、Jaeger、Zipkin和Application Insights等可观测性平台集成,构建端到端的分布式追踪系统。

# 11. 地理位置感知中间件

用途: 基于用户地理位置提供本地化内容和服务,实现地域感知功能。

实现:

public class GeoLocationOptions
{
    public string IpStackApiKey { get; set; }
    public string MaxMindDbPath { get; set; }
    public GeoProviderType ProviderType { get; set; } = GeoProviderType.MaxMind;
    public TimeSpan CacheDuration { get; set; } = TimeSpan.FromDays(7);
    public bool UseCache { get; set; } = true;
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public List<string> TrustedProxies { get; set; } = new List<string>();
    public bool EnableRedirection { get; set; } = false;
    public Dictionary<string, string> CountryRedirectMap { get; set; } = new Dictionary<string, string>();
}

public enum GeoProviderType
{
    MaxMind,
    IpStack,
    Custom
}

public class GeoLocation
{
    public string IpAddress { get; set; }
    public string CountryCode { get; set; }
    public string CountryName { get; set; }
    public string RegionCode { get; set; }
    public string RegionName { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public double? Latitude { get; set; }
    public double? Longitude { get; set; }
    public string TimeZone { get; set; }
    public string ContinentCode { get; set; }
    public bool IsEuropeanUnion { get; set; }
}

public interface IGeoProvider
{
    Task<GeoLocation> GetLocationAsync(string ipAddress);
}

public class MaxMindGeoProvider : IGeoProvider, IDisposable
{
    private readonly string _dbPath;
    private DatabaseReader _reader;
    private readonly ILogger<MaxMindGeoProvider> _logger;
    
    public MaxMindGeoProvider(string dbPath, ILogger<MaxMindGeoProvider> logger)
    {
        _dbPath = dbPath;
        _logger = logger;
        InitializeReader();
    }
    
    private void InitializeReader()
    {
        try
        {
            _reader = new DatabaseReader(_dbPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize MaxMind database from {DbPath}", _dbPath);
        }
    }
    
    public async Task<GeoLocation> GetLocationAsync(string ipAddress)
    {
        if (_reader == null)
        {
            return null;
        }
        
        try
        {
            // 解析IP地址
            if (!IPAddress.TryParse(ipAddress, out var parsedIp))
            {
                _logger.LogWarning("Invalid IP address: {IpAddress}", ipAddress);
                return null;
            }
            
            // 查询数据库
            var response = await _reader.CityAsync(parsedIp);
            if (response == null)
            {
                return null;
            }
            
            var location = new GeoLocation
            {
                IpAddress = ipAddress,
                CountryCode = response.Country?.IsoCode,
                CountryName = response.Country?.Name,
                RegionCode = response.MostSpecificSubdivision?.IsoCode,
                RegionName = response.MostSpecificSubdivision?.Name,
                City = response.City?.Name,
                PostalCode = response.Postal?.Code,
                Latitude = response.Location?.Latitude,
                Longitude = response.Location?.Longitude,
                TimeZone = response.Location?.TimeZone,
                ContinentCode = response.Continent?.Code,
                IsEuropeanUnion = response.Country?.IsInEuropeanUnion ?? false
            };
            
            return location;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error querying MaxMind database for IP {IpAddress}", ipAddress);
            return null;
        }
    }
    
    public void Dispose()
    {
        _reader?.Dispose();
    }
}

public class IpStackGeoProvider : IGeoProvider
{
    private readonly string _apiKey;
    private readonly HttpClient _httpClient;
    private readonly ILogger<IpStackGeoProvider> _logger;
    
    public IpStackGeoProvider(string apiKey, HttpClient httpClient, ILogger<IpStackGeoProvider> logger)
    {
        _apiKey = apiKey;
        _httpClient = httpClient;
        _logger = logger;
    }
    
    public async Task<GeoLocation> GetLocationAsync(string ipAddress)
    {
        try
        {
            // 构建API请求URL
            var requestUrl = $"http://api.ipstack.com/{ipAddress}?access_key={_apiKey}&format=1";
            
            // 发送请求
            var response = await _httpClient.GetAsync(requestUrl);
            if (!response.IsSuccessStatusCode)
            {
                _logger.LogWarning("Failed to get location from IpStack. Status code: {StatusCode}", response.StatusCode);
                return null;
            }
            
            // 解析响应
            var content = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<IpStackResponse>(content);
            
            if (result == null || !string.IsNullOrEmpty(result.Error?.Info))
            {
                _logger.LogWarning("IpStack API error: {Error}", result?.Error?.Info);
                return null;
            }
            
            var location = new GeoLocation
            {
                IpAddress = ipAddress,
                CountryCode = result.CountryCode,
                CountryName = result.CountryName,
                RegionCode = result.RegionCode,
                RegionName = result.RegionName,
                City = result.City,
                PostalCode = result.Zip,
                Latitude = result.Latitude,
                Longitude = result.Longitude,
                TimeZone = result.TimeZone?.Id,
                ContinentCode = result.ContinentCode,
                IsEuropeanUnion = result.IsEu
            };
            
            return location;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error querying IpStack API for IP {IpAddress}", ipAddress);
            return null;
        }
    }
    
    private class IpStackResponse
    {
        [JsonPropertyName("ip")]
        public string Ip { get; set; }
        
        [JsonPropertyName("country_code")]
        public string CountryCode { get; set; }
        
        [JsonPropertyName("country_name")]
        public string CountryName { get; set; }
        
        [JsonPropertyName("region_code")]
        public string RegionCode { get; set; }
        
        [JsonPropertyName("region_name")]
        public string RegionName { get; set; }
        
        [JsonPropertyName("city")]
        public string City { get; set; }
        
        [JsonPropertyName("zip")]
        public string Zip { get; set; }
        
        [JsonPropertyName("latitude")]
        public double Latitude { get; set; }
        
        [JsonPropertyName("longitude")]
        public double Longitude { get; set; }
        
        [JsonPropertyName("time_zone")]
        public TimeZoneInfo TimeZone { get; set; }
        
        [JsonPropertyName("continent_code")]
        public string ContinentCode { get; set; }
        
        [JsonPropertyName("continent_name")]
        public string ContinentName { get; set; }
        
        [JsonPropertyName("currency")]
        public CurrencyInfo Currency { get; set; }
        
        [JsonPropertyName("connection")]
        public ConnectionInfo Connection { get; set; }
        
        [JsonPropertyName("security")]
        public SecurityInfo Security { get; set; }
        
        [JsonPropertyName("is_eu")]
        public bool IsEu { get; set; }
        
        [JsonPropertyName("error")]
        public ErrorInfo Error { get; set; }
        
        public class TimeZoneInfo
        {
            [JsonPropertyName("id")]
            public string Id { get; set; }
            
            [JsonPropertyName("current_time")]
            public string CurrentTime { get; set; }
            
            [JsonPropertyName("gmt_offset")]
            public int GmtOffset { get; set; }
            
            [JsonPropertyName("code")]
            public string Code { get; set; }
            
            [JsonPropertyName("is_daylight_saving")]
            public bool IsDaylightSaving { get; set; }
        }
        
        public class CurrencyInfo
        {
            [JsonPropertyName("code")]
            public string Code { get; set; }
            
            [JsonPropertyName("name")]
            public string Name { get; set; }
            
            [JsonPropertyName("plural")]
            public string Plural { get; set; }
            
            [JsonPropertyName("symbol")]
            public string Symbol { get; set; }
            
            [JsonPropertyName("symbol_native")]
            public string SymbolNative { get; set; }
        }
        
        public class ConnectionInfo
        {
            [JsonPropertyName("asn")]
            public int Asn { get; set; }
            
            [JsonPropertyName("isp")]
            public string Isp { get; set; }
        }
        
        public class SecurityInfo
        {
            [JsonPropertyName("is_proxy")]
            public bool IsProxy { get; set; }
            
            [JsonPropertyName("proxy_type")]
            public string ProxyType { get; set; }
            
            [JsonPropertyName("is_crawler")]
            public bool IsCrawler { get; set; }
            
            [JsonPropertyName("crawler_name")]
            public string CrawlerName { get; set; }
            
            [JsonPropertyName("crawler_type")]
            public string CrawlerType { get; set; }
            
            [JsonPropertyName("is_tor")]
            public bool IsTor { get; set; }
            
            [JsonPropertyName("threat_level")]
            public string ThreatLevel { get; set; }
            
            [JsonPropertyName("threat_types")]
            public string[] ThreatTypes { get; set; }
        }
        
        public class ErrorInfo
        {
            [JsonPropertyName("code")]
            public int Code { get; set; }
            
            [JsonPropertyName("type")]
            public string Type { get; set; }
            
            [JsonPropertyName("info")]
            public string Info { get; set; }
        }
    }
}

public class GeoLocationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly GeoLocationOptions _options;
    private readonly ILogger<GeoLocationMiddleware> _logger;
    private readonly IGeoProvider _geoProvider;
    private readonly IMemoryCache _cache;
    
    public GeoLocationMiddleware(
        RequestDelegate next,
        GeoLocationOptions options,
        ILogger<GeoLocationMiddleware> logger,
        IGeoProvider geoProvider,
        IMemoryCache cache)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _geoProvider = geoProvider;
        _cache = cache;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该跳过地理位置处理
        if (ShouldSkipGeoLocation(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 获取客户端IP
        var clientIp = GetClientIp(context, _options.TrustedProxies);
        if (string.IsNullOrEmpty(clientIp))
        {
            _logger.LogWarning("Unable to determine client IP address");
            await _next(context);
            return;
        }
        
        // 获取地理位置信息
        var location = await GetGeoLocationAsync(clientIp);
        
        if (location != null)
        {
            // 将地理位置信息存储在HttpContext中
            context.Items["GeoLocation"] = location;
            
            // 添加常用的地理位置头部
            context.Response.Headers["X-Country-Code"] = location.CountryCode;
            context.Response.Headers["X-Region-Code"] = location.RegionCode;
            
            // 检查是否需要基于地理位置重定向
            if (_options.EnableRedirection && !string.IsNullOrEmpty(location.CountryCode))
            {
                if (_options.CountryRedirectMap.TryGetValue(location.CountryCode, out var redirectUrl))
                {
                    // 避免无限重定向循环
                    if (!context.Request.Path.StartsWithSegments(redirectUrl))
                    {
                        _logger.LogInformation("Redirecting user from {CountryCode} to {RedirectUrl}", 
                            location.CountryCode, redirectUrl);
                        
                        context.Response.Redirect(redirectUrl);
                        return;
                    }
                }
            }
        }
        
        await _next(context);
    }

    private bool ShouldSkipGeoLocation(HttpRequest request)
    {
        return _options.ExcludedPaths.Any(p => 
            request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase));
    }

    private string GetClientIp(HttpContext context, List<string> trustedProxies)
    {
        // 尝试从X-Forwarded-For头获取真实IP
        if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
        {
            // X-Forwarded-For格式: client, proxy1, proxy2, ...
            var ips = forwardedFor.ToString().Split(',', StringSplitOptions.RemoveEmptyEntries);
            if (ips.Length > 0)
            {
                var clientIp = ips[0].Trim();
                
                // 验证代理链
                if (ips.Length == 1 || 
                    ips.Skip(1).All(ip => trustedProxies.Contains(ip.Trim())))
                {
                    return clientIp;
                }
            }
        }
        
        // 尝试从X-Real-IP头获取
        if (context.Request.Headers.TryGetValue("X-Real-IP", out var realIp))
        {
            return realIp.ToString().Trim();
        }
        
        // 使用远程IP地址
        return context.Connection.RemoteIpAddress?.ToString();
    }

    private async Task<GeoLocation> GetGeoLocationAsync(string ipAddress)
    {
        // 检查是否为本地或私有IP
        if (IsLocalOrPrivateIp(ipAddress))
        {
            _logger.LogDebug("Skipping geolocation for local/private IP: {IpAddress}", ipAddress);
            return null;
        }
        
        // 如果启用了缓存,从缓存中获取
        var cacheKey = $"geo:{ipAddress}";
        
        if (_options.UseCache && _cache.TryGetValue(cacheKey, out GeoLocation cachedLocation))
        {
            return cachedLocation;
        }
        
        // 从提供者获取地理位置
        var location = await _geoProvider.GetLocationAsync(ipAddress);
        
        // 缓存结果
        if (location != null && _options.UseCache)
        {
            _cache.Set(cacheKey, location, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = _options.CacheDuration
            });
        }
        
        return location;
    }

    private bool IsLocalOrPrivateIp(string ipAddress)
    {
        if (ipAddress == "127.0.0.1" || ipAddress == "::1" || ipAddress == "localhost")
        {
            return true;
        }
        
        if (IPAddress.TryParse(ipAddress, out var parsedIp))
        {
            byte[] bytes = parsedIp.GetAddressBytes();
            
            // 10.0.0.0/8
            if (parsedIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && bytes[0] == 10)
            {
                return true;
            }
            
            // 172.16.0.0/12
            if (parsedIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && 
                bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31)
            {
                return true;
            }
            
            // 192.168.0.0/16
            if (parsedIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && 
                bytes[0] == 192 && bytes[1] == 168)
            {
                return true;
            }
            
            // fc00::/7
            if (parsedIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 && 
                (bytes[0] & 0xfe) == 0xfc)
            {
                return true;
            }
        }
        
        return false;
    }
}

public static class GeoLocationMiddlewareExtensions
{
    public static IServiceCollection AddGeoLocation(
        this IServiceCollection services,
        Action<GeoLocationOptions> configureOptions = null)
    {
        var options = new GeoLocationOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        services.AddMemoryCache();
        services.AddHttpClient();
        
        // 注册地理位置提供者
        switch (options.ProviderType)
        {
            case GeoProviderType.MaxMind:
                if (string.IsNullOrEmpty(options.MaxMindDbPath))
                {
                    throw new ArgumentException("MaxMindDbPath must be specified when using MaxMind provider");
                }
                
                services.AddSingleton<IGeoProvider>(sp => new MaxMindGeoProvider(
                    options.MaxMindDbPath,
                    sp.GetRequiredService<ILogger<MaxMindGeoProvider>>()));
                break;
                
            case GeoProviderType.IpStack:
                if (string.IsNullOrEmpty(options.IpStackApiKey))
                {
                    throw new ArgumentException("IpStackApiKey must be specified when using IpStack provider");
                }
                
                services.AddSingleton<IGeoProvider>(sp => new IpStackGeoProvider(
                    options.IpStackApiKey,
                    sp.GetRequiredService<HttpClient>(),
                    sp.GetRequiredService<ILogger<IpStackGeoProvider>>()));
                break;
                
            case GeoProviderType.Custom:
                // 需要用户注册自定义IGeoProvider实现
                break;
                
            default:
                throw new NotSupportedException($"GeoProvider type '{options.ProviderType}' is not supported");
        }
        
        return services;
    }
    
    public static IApplicationBuilder UseGeoLocation(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<GeoLocationMiddleware>();
    }
    
    // 获取地理位置的扩展方法
    public static GeoLocation GetGeoLocation(this HttpContext context)
    {
        if (context.Items.TryGetValue("GeoLocation", out var location))
        {
            return location as GeoLocation;
        }
        
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572

注册方式:

// 在Startup.ConfigureServices中
services.AddGeoLocation(options => {
    options.ProviderType = GeoProviderType.MaxMind;
    options.MaxMindDbPath = Path.Combine(env.ContentRootPath, "GeoLite2-City.mmdb");
    options.CacheDuration = TimeSpan.FromDays(30);
    options.EnableRedirection = true;
    options.CountryRedirectMap = new Dictionary<string, string>
    {
        ["GB"] = "/uk",
        ["FR"] = "/fr",
        ["DE"] = "/de"
    };
    options.TrustedProxies.Add("10.0.0.1");
    options.ExcludedPaths.Add("/api");
});

// 在Startup.Configure中
app.UseGeoLocation();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用示例:

// 在控制器中访问地理位置
public class HomeController : Controller
{
    [HttpGet]
    public IActionResult Index()
    {
        var location = HttpContext.GetGeoLocation();
        
        if (location != null)
        {
            ViewBag.CountryName = location.CountryName;
            ViewBag.City = location.City;
            
            // 根据地理位置自定义内容
            if (location.CountryCode == "US")
            {
                return View("US_Index");
            }
        }
        
        return View();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

应用场景: 为不同国家/地区的用户提供本地化内容,实现地域定价和合规性,以及基于位置的服务发现。适用于电子商务、流媒体和内容分发网络。

市场集成: 与MaxMind GeoIP2、IP-API或ipstack等地理位置服务集成;与CDN服务集成,实现边缘位置感知功能。

# 12. 静态资源优化中间件

用途: 自动压缩、捆绑和优化静态资源(CSS、JavaScript、图像等),提高网站性能。

实现:

public class StaticResourceOptions
{
    public bool EnableCompression { get; set; } = true;
    public bool EnableBundling { get; set; } = true;
    public bool EnableMinification { get; set; } = true;
    public bool EnableVersioning { get; set; } = true;
    public bool EnableBrowserCaching { get; set; } = true;
    public TimeSpan CacheDuration { get; set; } = TimeSpan.FromDays(365);
    public Dictionary<string, string[]> Bundles { get; set; } = new Dictionary<string, string[]>();
    public List<string> ExcludedExtensions { get; set; } = new List<string>();
    public Dictionary<string, string> ContentTypes { get; set; } = new Dictionary<string, string>();
    public int ImageQuality { get; set; } = 80;
    public bool OptimizeImages { get; set; } = true;
    public bool UseCdn { get; set; } = false;
    public string CdnUrl { get; set; }
}

public class StaticResourceMiddleware
{
    private readonly RequestDelegate _next;
    private readonly StaticResourceOptions _options;
    private readonly IWebHostEnvironment _env;
    private readonly ILogger<StaticResourceMiddleware> _logger;
    private readonly IMemoryCache _cache;
    private readonly Dictionary<string, string> _bundleCache = new Dictionary<string, string>();
    private readonly Dictionary<string, byte[]> _compressedCache = new Dictionary<string, byte[]>();
    
    // 最常见的MIME类型映射
    private static readonly Dictionary<string, string> DefaultContentTypes = new Dictionary<string, string>
    {
        [".css"] = "text/css",
        [".js"] = "application/javascript",
        [".jpg"] = "image/jpeg",
        [".jpeg"] = "image/jpeg",
        [".png"] = "image/png",
        [".gif"] = "image/gif",
        [".svg"] = "image/svg+xml",
        [".webp"] = "image/webp",
        [".woff"] = "font/woff",
        [".woff2"] = "font/woff2",
        [".ttf"] = "font/ttf",
        [".eot"] = "application/vnd.ms-fontobject",
        [".map"] = "application/json",
        [".ico"] = "image/x-icon",
        [".json"] = "application/json",
        [".xml"] = "application/xml",
        [".txt"] = "text/plain",
        [".html"] = "text/html"
    };

    public StaticResourceMiddleware(
        RequestDelegate next,
        StaticResourceOptions options,
        IWebHostEnvironment env,
        ILogger<StaticResourceMiddleware> logger,
        IMemoryCache cache)
    {
        _next = next;
        _options = options;
        _env = env;
        _logger = logger;
        _cache = cache;
        
        // 合并自定义内容类型和默认类型
        foreach (var contentType in DefaultContentTypes)
        {
            if (!_options.ContentTypes.ContainsKey(contentType.Key))
            {
                _options.ContentTypes[contentType.Key] = contentType.Value;
            }
        }
        
        // 预处理捆绑包
        if (_options.EnableBundling)
        {
            InitializeBundles();
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path;
        
        // 检查请求是否针对静态资源
        if (IsStaticResource(path, out var extension))
        {
            // 检查是否是捆绑请求
            if (_options.EnableBundling && path.Value.StartsWith("/bundles/", StringComparison.OrdinalIgnoreCase))
            {
                await ServeBundleAsync(context, path);
                return;
            }
            
            // 处理普通静态资源
            await ServeStaticResourceAsync(context, path, extension);
            return;
        }
        
        // 不是静态资源,继续下一个中间件
        await _next(context);
    }

    private bool IsStaticResource(PathString path, out string extension)
    {
        extension = Path.GetExtension(path.Value).ToLowerInvariant();
        
        if (string.IsNullOrEmpty(extension))
        {
            return false;
        }
        
        // 检查是否在排除列表中
        if (_options.ExcludedExtensions.Contains(extension))
        {
            return false;
        }
        
        // 检查是否为已知的静态资源类型
        return _options.ContentTypes.ContainsKey(extension);
    }

    private void InitializeBundles()
    {
        foreach (var bundle in _options.Bundles)
        {
            try
            {
                var bundlePath = $"/bundles/{bundle.Key}";
                var bundleContent = CreateBundle(bundle.Value);
                _bundleCache[bundlePath] = bundleContent;
                
                // 预压缩捆绑内容
                if (_options.EnableCompression)
                {
                    _compressedCache[bundlePath] = Compress(bundleContent);
                }
                
                _logger.LogInformation("Created bundle: {BundlePath}, Size: {Size}KB", 
                    bundlePath, bundleContent.Length / 1024);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error creating bundle {BundleKey}", bundle.Key);
            }
        }
    }

    private string CreateBundle(string[] files)
    {
        var content = new StringBuilder();
        var wwwrootPath = _env.WebRootPath;
        
        foreach (var file in files)
        {
            var filePath = Path.Combine(wwwrootPath, file.TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
            if (File.Exists(filePath))
            {
                var fileContent = File.ReadAllText(filePath);
                
                // 如果启用了缩小功能,尝试缩小内容
                if (_options.EnableMinification)
                {
                    var extension = Path.GetExtension(filePath).ToLowerInvariant();
                    if (extension == ".css")
                    {
                        fileContent = MinifyCss(fileContent);
                    }
                    else if (extension == ".js")
                    {
                        fileContent = MinifyJs(fileContent);
                    }
                }
                
                content.AppendLine(fileContent);
                content.AppendLine();
            }
            else
            {
                _logger.LogWarning("File not found for bundle: {FilePath}", filePath);
            }
        }
        
        return content.ToString();
    }

    private string MinifyCss(string css)
    {
        // 简单的CSS压缩:删除注释、空白和换行符
        // 注意:在生产环境中,应使用专业的CSS压缩库
        css = Regex.Replace(css, @"/\*[\d\D]*?\*/", string.Empty); // 删除注释
        css = Regex.Replace(css, @"[^\S\r\n]+", " "); // 将多个空格替换为单个空格
        css = Regex.Replace(css, @"\s*([,:;{}>])\s*", "$1"); // 删除选择器和属性周围的空格
        css = Regex.Replace(css, @"\s*([{}])\s*", "$1"); // 删除括号周围的空格
        css = Regex.Replace(css, @";}", "}"); // 删除最后一个分号
        css = Regex.Replace(css, @"[\r\n]+", string.Empty); // 删除换行符
        
        return css;
    }

    private string MinifyJs(string js)
    {
        // 简单的JS压缩:删除注释和一些空白
        // 注意:在生产环境中,应使用专业的JS压缩库(如UglifyJS)
        js = Regex.Replace(js, @"//.*?$", string.Empty, RegexOptions.Multiline); // 删除单行注释
        js = Regex.Replace(js, @"/\*[\d\D]*?\*/", string.Empty); // 删除多行注释
        js = Regex.Replace(js, @"^\s+", string.Empty, RegexOptions.Multiline); // 删除行首空白
        js = Regex.Replace(js, @"\s+$", string.Empty, RegexOptions.Multiline); // 删除行尾空白
        js = Regex.Replace(js, @"([,;:{}()[\]])\s+", "$1", RegexOptions.Multiline); // 删除标点后空白
        js = Regex.Replace(js, @"\s+([,;:{}()[\]])", "$1", RegexOptions.Multiline); // 删除标点前空白
        
        return js;
    }

    private byte[] Compress(string content)
    {
        using var outputStream = new MemoryStream();
        using (var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal))
        {
            using var writer = new StreamWriter(gzipStream);
            writer.Write(content);
        }
        
        return outputStream.ToArray();
    }

    private async Task ServeBundleAsync(HttpContext context, PathString path)
    {
        if (!_bundleCache.TryGetValue(path, out var content))
        {
            context.Response.StatusCode = 404;
            return;
        }
        
        var acceptEncoding = context.Request.Headers["Accept-Encoding"].ToString().ToLowerInvariant();
        var supportsGzip = acceptEncoding.Contains("gzip");
        
        context.Response.ContentType = GetContentType(path);
        
        // 设置缓存头
        if (_options.EnableBrowserCaching)
        {
            AddCacheHeaders(context.Response);
        }
        
        // 设置版本头
        if (_options.EnableVersioning)
        {
            var etag = $"W/\"{GenerateETag(content)}\"";
            context.Response.Headers["ETag"] = etag;
            
            // 检查条件请求
            var ifNoneMatch = context.Request.Headers["If-None-Match"].ToString();
            if (!string.IsNullOrEmpty(ifNoneMatch) && ifNoneMatch == etag)
            {
                context.Response.StatusCode = 304; // Not Modified
                return;
            }
        }
        
        // 使用压缩(如果客户端支持)
        if (_options.EnableCompression && supportsGzip && _compressedCache.TryGetValue(path, out var compressedContent))
        {
            context.Response.Headers["Content-Encoding"] = "gzip";
            await context.Response.Body.WriteAsync(compressedContent, 0, compressedContent.Length);
        }
        else
        {
            await context.Response.WriteAsync(content);
        }
    }

    private async Task ServeStaticResourceAsync(HttpContext context, PathString path, string extension)
    {
        // 获取物理文件路径
        var relativePath = path.Value.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
        var absolutePath = Path.Combine(_env.WebRootPath, relativePath);
        
        // 检查文件是否存在
        if (!File.Exists(absolutePath))
        {
            await _next(context);
            return;
        }
        
        // 获取文件信息
        var fileInfo = new FileInfo(absolutePath);
        var lastModified = fileInfo.LastWriteTimeUtc;
        var contentType = GetContentType(path);
        
        // 设置内容类型
        context.Response.ContentType = contentType;
        
        // 设置缓存头
        if (_options.EnableBrowserCaching)
        {
            AddCacheHeaders(context.Response);
        }
        
        // 处理条件请求
        var ifModifiedSince = context.Request.Headers["If-Modified-Since"].ToString();
        if (!string.IsNullOrEmpty(ifModifiedSince) && 
            DateTime.TryParse(ifModifiedSince, out var ifModifiedSinceDate))
        {
            // 使用1秒的精度比较(HTTP日期标头不包含毫秒)
            if (lastModified.AddSeconds(-lastModified.Second % 1) <= 
                ifModifiedSinceDate.AddSeconds(-ifModifiedSinceDate.Second % 1))
            {
                context.Response.StatusCode = 304; // Not Modified
                return;
            }
        }
        
        // 设置Last-Modified头
        context.Response.Headers["Last-Modified"] = lastModified.ToString("R");
        
        // 处理图像优化
        if (_options.OptimizeImages && IsImageExtension(extension))
        {
            await ServeOptimizedImageAsync(context, absolutePath, extension);
            return;
        }
        
        // 常规文件服务
        var acceptEncoding = context.Request.Headers["Accept-Encoding"].ToString().ToLowerInvariant();
        var supportsGzip = acceptEncoding.Contains("gzip");
        
        if (_options.EnableCompression && supportsGzip && IsCompressibleContent(contentType))
        {
            await ServeCompressedFileAsync(context, absolutePath, contentType);
            return;
        }
        
        await ServeFileAsync(context, absolutePath);
    }

    private string GetContentType(PathString path)
    {
        var extension = Path.GetExtension(path.Value).ToLowerInvariant();
        
        if (_options.ContentTypes.TryGetValue(extension, out var contentType))
        {
            return contentType;
        }
        
        return "application/octet-stream";
    }

    private bool IsImageExtension(string extension)
    {
        return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || 
               extension == ".gif" || extension == ".webp";
    }

    private bool IsCompressibleContent(string contentType)
    {
        return contentType.StartsWith("text/") || 
               contentType == "application/javascript" || 
               contentType == "application/json" || 
               contentType == "application/xml" || 
               contentType.Contains("application/font") || 
               contentType.Contains("/svg+xml");
    }

    private void AddCacheHeaders(HttpResponse response)
    {
        var maxAge = (int)_options.CacheDuration.TotalSeconds;
        response.Headers["Cache-Control"] = $"public, max-age={maxAge}";
        response.Headers["Expires"] = DateTime.UtcNow.Add(_options.CacheDuration).ToString("R");
    }

    private string GenerateETag(string content)
    {
        using var md5 = MD5.Create();
        var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(content));
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }

    private async Task ServeCompressedFileAsync(HttpContext context, string filePath, string contentType)
    {
        // 尝试从缓存获取压缩内容
        var cacheKey = $"compressed:{filePath}:{File.GetLastWriteTimeUtc(filePath).Ticks}";
        
        if (!_cache.TryGetValue(cacheKey, out byte[] compressedContent))
        {
            // 读取文件内容
            var fileContent = await File.ReadAllTextAsync(filePath);
            
            // 应用缩小(如果适用)
            if (_options.EnableMinification)
            {
                var extension = Path.GetExtension(filePath).ToLowerInvariant();
                if (extension == ".css")
                {
                    fileContent = MinifyCss(fileContent);
                }
                else if (extension == ".js")
                {
                    fileContent = MinifyJs(fileContent);
                }
            }
            
            // 压缩内容
            compressedContent = Compress(fileContent);
            
            // 缓存压缩内容
            _cache.Set(cacheKey, compressedContent, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
                Size = compressedContent.Length
            });
        }
        
        // 设置压缩头部
        context.Response.Headers["Content-Encoding"] = "gzip";
        
        // 发送压缩内容
        await context.Response.Body.WriteAsync(compressedContent, 0, compressedContent.Length);
    }

    private async Task ServeOptimizedImageAsync(HttpContext context, string filePath, string extension)
    {
        // 在真实场景中,应使用专业图像处理库(如ImageSharp)
        // 此处简化实现
        await ServeFileAsync(context, filePath);
    }

    private async Task ServeFileAsync(HttpContext context, string filePath)
    {
        // 获取CDN URL(如果启用)
        if (_options.UseCdn && !string.IsNullOrEmpty(_options.CdnUrl))
        {
            var relPath = filePath.Substring(_env.WebRootPath.Length).Replace('\\', '/');
            var cdnUrl = $"{_options.CdnUrl.TrimEnd('/')}{relPath}";
            
            context.Response.Redirect(cdnUrl);
            return;
        }
        
        // 直接服务文件
        await context.Response.SendFileAsync(filePath);
    }
}

public static class StaticResourceMiddlewareExtensions
{
    public static IServiceCollection AddStaticResourceOptimization(
        this IServiceCollection services,
        Action<StaticResourceOptions> configureOptions = null)
    {
        var options = new StaticResourceOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        services.AddMemoryCache();
        
        return services;
    }
    
    public static IApplicationBuilder UseStaticResourceOptimization(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<StaticResourceMiddleware>();
    }
    
    // 生成捆绑URL的扩展方法
    public static string GetBundleUrl(this IUrlHelper url, string bundleName)
    {
        return url.Content($"~/bundles/{bundleName}");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469

注册方式:

// 在Startup.ConfigureServices中
services.AddStaticResourceOptimization(options => {
    options.EnableBundling = true;
    options.EnableMinification = true;
    options.EnableCompression = true;
    options.EnableVersioning = true;
    options.CacheDuration = TimeSpan.FromDays(365);
    
    // 定义捆绑包
    options.Bundles["main.css"] = new[] { 
        "/css/normalize.css", 
        "/css/base.css", 
        "/css/layout.css" 
    };
    
    options.Bundles["vendor.js"] = new[] { 
        "/js/jquery.min.js", 
        "/js/bootstrap.js" 
    };
    
    options.Bundles["app.js"] = new[] { 
        "/js/utils.js", 
        "/js/components.js", 
        "/js/app.js" 
    };
    
    // 可选:CDN配置
    options.UseCdn = false;
    options.CdnUrl = "https://cdn.example.com";
});

// 在Startup.Configure中
// 注意:必须在UseStaticFiles()之前添加
app.UseStaticResourceOptimization();
app.UseStaticFiles();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

使用示例:

@* 在Razor视图中使用捆绑包 *@
<link rel="stylesheet" href="@Url.GetBundleUrl("main.css")" />
<script src="@Url.GetBundleUrl("vendor.js")"></script>
<script src="@Url.GetBundleUrl("app.js")"></script>
1
2
3
4

应用场景: 提高网站性能,减少加载时间和带宽使用。特别适用于内容丰富的网站、电子商务平台和企业门户。

市场集成: 与现有的前端构建工具(如WebPack、Gulp)集成;与CDN服务(如Cloudflare、Azure CDN)集成,实现边缘缓存。

# 13. 文件上传处理中间件

用途: 安全高效地处理文件上传,支持大文件、多文件和格式验证。

实现:

public class FileUploadOptions
{
    public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB
    public int MaxFiles { get; set; } = 10;
    public string UploadPath { get; set; } = "uploads";
    public bool CreateUploadPathIfNotExists { get; set; } = true;
    public bool UseUniqueFileNames { get; set; } = true;
    public bool ScanForViruses { get; set; } = false;
    public string VirusScannerPath { get; set; }
    public List<string> AllowedExtensions { get; set; } = new List<string>();
    public List<string> AllowedMimeTypes { get; set; } = new List<string>();
    public List<string> UploadEndpoints { get; set; } = new List<string>();
    public bool EnableChunkUploads { get; set; } = true;
    public string ChunkUploadEndpoint { get; set; } = "/api/upload/chunk";
    public string TempChunkDir { get; set; } = "chunks";
    public int ChunkSize { get; set; } = 1024 * 1024; // 1MB
    public TimeSpan ChunkExpiration { get; set; } = TimeSpan.FromHours(24);
    public bool ValidateFileContent { get; set; } = true;
    public bool ResizeImages { get; set; } = false;
    public int MaxImageWidth { get; set; } = 1920;
    public int MaxImageHeight { get; set; } = 1080;
    public int ImageQuality { get; set; } = 80;
}

public class FileUploadResult
{
    public bool Success { get; set; }
    public string FileName { get; set; }
    public string StoredFileName { get; set; }
    public string RelativePath { get; set; }
    public string AbsolutePath { get; set; }
    public string ContentType { get; set; }
    public long FileSize { get; set; }
    public string Error { get; set; }
    public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
}

public class ChunkInfo
{
    public Guid FileId { get; set; }
    public string FileName { get; set; }
    public long ChunkIndex { get; set; }
    public long TotalChunks { get; set; }
    public long ChunkSize { get; set; }
    public long TotalSize { get; set; }
    public string ContentType { get; set; }
    public DateTime LastUpdated { get; set; }
}

public class FileUploadMiddleware
{
    private readonly RequestDelegate _next;
    private readonly FileUploadOptions _options;
    private readonly IWebHostEnvironment _env;
    private readonly ILogger<FileUploadMiddleware> _logger;
    private readonly ConcurrentDictionary<Guid, ChunkInfo> _chunksMetadata = new ConcurrentDictionary<Guid, ChunkInfo>();
    
    // 常见文件类型的MIME映射
    private static readonly Dictionary<string, string[]> ContentTypeMapping = new Dictionary<string, string[]>
    {
        { ".jpg", new[] { "image/jpeg" } },
        { ".jpeg", new[] { "image/jpeg" } },
        { ".png", new[] { "image/png" } },
        { ".gif", new[] { "image/gif" } },
        { ".pdf", new[] { "application/pdf" } },
        { ".doc", new[] { "application/msword" } },
        { ".docx", new[] { "application/vnd.openxmlformats-officedocument.wordprocessingml.document" } },
        { ".xls", new[] { "application/vnd.ms-excel" } },
        { ".xlsx", new[] { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } },
        { ".csv", new[] { "text/csv", "application/csv" } },
        { ".txt", new[] { "text/plain" } },
        { ".zip", new[] { "application/zip" } },
        { ".xml", new[] { "text/xml", "application/xml" } },
        { ".mp4", new[] { "video/mp4" } },
        { ".mp3", new[] { "audio/mpeg" } }
    };

    public FileUploadMiddleware(
        RequestDelegate next,
        FileUploadOptions options,
        IWebHostEnvironment env,
        ILogger<FileUploadMiddleware> logger)
    {
        _next = next;
        _options = options;
        _env = env;
        _logger = logger;
        
        // 创建上传目录(如果不存在)
        if (_options.CreateUploadPathIfNotExists)
        {
            EnsureDirectoryExists(GetAbsoluteUploadPath());
            
            if (_options.EnableChunkUploads)
            {
                EnsureDirectoryExists(GetChunksPath());
            }
        }
        
        // 启动后台任务以清理过期的分块上传
        if (_options.EnableChunkUploads)
        {
            StartChunkCleanupTask();
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否是文件上传请求
        if (IsUploadRequest(context.Request))
        {
            await HandleFileUploadAsync(context);
            return;
        }
        
        // 检查是否是分块上传请求
        if (_options.EnableChunkUploads && IsChunkUploadRequest(context.Request))
        {
            await HandleChunkUploadAsync(context);
            return;
        }
        
        await _next(context);
    }

    private bool IsUploadRequest(HttpRequest request)
    {
        if (!HttpMethods.IsPost(request.Method))
        {
            return false;
        }
        
        if (!request.HasFormContentType)
        {
            return false;
        }
        
        if (_options.UploadEndpoints.Count > 0)
        {
            return _options.UploadEndpoints.Any(endpoint => 
                request.Path.StartsWithSegments(endpoint, StringComparison.OrdinalIgnoreCase));
        }
        
        return true;
    }

    private bool IsChunkUploadRequest(HttpRequest request)
    {
        return HttpMethods.IsPost(request.Method) && 
               request.Path.Equals(_options.ChunkUploadEndpoint, StringComparison.OrdinalIgnoreCase);
    }

    private async Task HandleFileUploadAsync(HttpContext context)
    {
        var results = new List<FileUploadResult>();
        var form = await context.Request.ReadFormAsync();
        var files = form.Files;
        
        // 检查文件数量
        if (files.Count > _options.MaxFiles)
        {
            await RespondWithErrorAsync(context, $"Too many files. Maximum allowed: {_options.MaxFiles}");
            return;
        }
        
        // 处理每个文件
        foreach (var file in files)
        {
            var result = await ProcessFileAsync(file);
            results.Add(result);
        }
        
        // 返回处理结果
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(results);
    }

    private async Task<FileUploadResult> ProcessFileAsync(IFormFile file)
    {
        var result = new FileUploadResult
        {
            FileName = file.FileName,
            ContentType = file.ContentType,
            FileSize = file.Length
        };
        
        try
        {
            // 检查文件大小
            if (file.Length > _options.MaxFileSize)
            {
                result.Success = false;
                result.Error = $"File size exceeds the limit of {_options.MaxFileSize / 1024 / 1024}MB";
                return result;
            }
            
            // 获取文件扩展名
            var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
            
            // 验证文件扩展名
            if (_options.AllowedExtensions.Count > 0 && !_options.AllowedExtensions.Contains(extension))
            {
                result.Success = false;
                result.Error = $"File extension '{extension}' is not allowed";
                return result;
            }
            
            // 验证MIME类型
            if (_options.AllowedMimeTypes.Count > 0 && !_options.AllowedMimeTypes.Contains(file.ContentType))
            {
                result.Success = false;
                result.Error = $"File type '{file.ContentType}' is not allowed";
                return result;
            }
            
            // 验证文件内容与扩展名的一致性
            if (_options.ValidateFileContent && !await ValidateFileContentAsync(file, extension))
            {
                result.Success = false;
                result.Error = "File content doesn't match the declared type";
                return result;
            }
            
            // 生成存储文件名
            var storedFileName = _options.UseUniqueFileNames
                ? $"{Guid.NewGuid()}{extension}"
                : file.FileName;
                
            // 确保文件名安全
            storedFileName = MakeSafeFileName(storedFileName);
            
            // 构建相对路径和绝对路径
            var relativePath = Path.Combine(_options.UploadPath, storedFileName);
            var absolutePath = Path.Combine(_env.WebRootPath, relativePath);
            
            // 创建目录(如果不存在)
            var directory = Path.GetDirectoryName(absolutePath);
            EnsureDirectoryExists(directory);
            
            // 保存文件
            using (var stream = new FileStream(absolutePath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }
            
            // 病毒扫描(如果启用)
            if (_options.ScanForViruses && !await ScanFileForVirusesAsync(absolutePath))
            {
                // 删除有病毒的文件
                File.Delete(absolutePath);
                
                result.Success = false;
                result.Error = "File may contain malware and was rejected";
                return result;
            }
            
            // 调整图像大小(如果启用)
            if (_options.ResizeImages && IsImageFile(extension))
            {
                await ResizeImageAsync(absolutePath);
            }
            
            // 设置结果
            result.Success = true;
            result.StoredFileName = storedFileName;
            result.RelativePath = relativePath.Replace('\\', '/');
            result.AbsolutePath = absolutePath;
            
            // 添加元数据
            result.Metadata["UploadTime"] = DateTime.UtcNow.ToString("o");
            result.Metadata["ContentType"] = file.ContentType;
            
            _logger.LogInformation("File uploaded successfully: {FileName}, Stored as: {StoredFileName}, Size: {Size}KB",
                file.FileName, storedFileName, file.Length / 1024);
        }
        catch (Exception ex)
        {
            result.Success = false;
            result.Error = $"Error uploading file: {ex.Message}";
            _logger.LogError(ex, "Error processing file upload: {FileName}", file.FileName);
        }
        
        return result;
    }

    private async Task HandleChunkUploadAsync(HttpContext context)
    {
        // 解析请求数据
        var form = await context.Request.ReadFormAsync();
        var file = form.Files.FirstOrDefault();
        
        if (file == null)
        {
            await RespondWithErrorAsync(context, "No file chunk found in the request");
            return;
        }
        
        // 获取分块信息
        if (!Guid.TryParse(form["fileId"], out var fileId))
        {
            await RespondWithErrorAsync(context, "Invalid file ID");
            return;
        }
        
        if (!long.TryParse(form["chunkIndex"], out var chunkIndex))
        {
            await RespondWithErrorAsync(context, "Invalid chunk index");
            return;
        }
        
        if (!long.TryParse(form["totalChunks"], out var totalChunks))
        {
            await RespondWithErrorAsync(context, "Invalid total chunks");
            return;
        }
        
        if (!long.TryParse(form["totalSize"], out var totalSize))
        {
            await RespondWithErrorAsync(context, "Invalid total size");
            return;
        }
        
        var fileName = form["fileName"].ToString();
        if (string.IsNullOrEmpty(fileName))
        {
            await RespondWithErrorAsync(context, "File name is required");
            return;
        }
        
        // 验证文件大小
        if (totalSize > _options.MaxFileSize)
        {
            await RespondWithErrorAsync(context, $"File size exceeds the limit of {_options.MaxFileSize / 1024 / 1024}MB");
            return;
        }
        
        try
        {
            // 保存或更新分块元数据
            var chunkInfo = _chunksMetadata.GetOrAdd(fileId, _ => new ChunkInfo
            {
                FileId = fileId,
                FileName = fileName,
                TotalChunks = totalChunks,
                TotalSize = totalSize,
                ContentType = file.ContentType,
                LastUpdated = DateTime.UtcNow
            });
            
            chunkInfo.ChunkIndex = chunkIndex;
            chunkInfo.ChunkSize = file.Length;
            chunkInfo.LastUpdated = DateTime.UtcNow;
            
            // 保存分块文件
            var chunkFilePath = Path.Combine(GetChunksPath(), $"{fileId}_{chunkIndex}");
            using (var stream = new FileStream(chunkFilePath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }
            
            // 检查是否所有分块都已上传
            var allChunksUploaded = true;
            for (var i = 0; i < totalChunks; i++)
            {
                var chunk = Path.Combine(GetChunksPath(), $"{fileId}_{i}");
                if (!File.Exists(chunk))
                {
                    allChunksUploaded = false;
                    break;
                }
            }
            
            if (allChunksUploaded)
            {
                // 合并所有分块
                var result = await MergeChunksAsync(fileId, chunkInfo);
                
                // 返回合并结果
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsJsonAsync(new
                {
                    success = true,
                    complete = true,
                    file = result
                });
            }
            else
            {
                // 返回分块上传进度
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsJsonAsync(new
                {
                    success = true,
                    complete = false,
                    chunkIndex,
                    totalChunks,
                    progress = (chunkIndex + 1) * 100 / totalChunks
                });
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing chunk upload: {FileName}, Chunk: {ChunkIndex}/{TotalChunks}",
                fileName, chunkIndex, totalChunks);
                
            await RespondWithErrorAsync(context, $"Error uploading chunk: {ex.Message}");
        }
    }

    private async Task<FileUploadResult> MergeChunksAsync(Guid fileId, ChunkInfo chunkInfo)
    {
        var extension = Path.GetExtension(chunkInfo.FileName).ToLowerInvariant();
        
        // 验证文件扩展名
        if (_options.AllowedExtensions.Count > 0 && !_options.AllowedExtensions.Contains(extension))
        {
            throw new InvalidOperationException($"File extension '{extension}' is not allowed");
        }
        
        // 验证MIME类型
        if (_options.AllowedMimeTypes.Count > 0 && !_options.AllowedMimeTypes.Contains(chunkInfo.ContentType))
        {
            throw new InvalidOperationException($"File type '{chunkInfo.ContentType}' is not allowed");
        }
        
        // 生成存储文件名
        var storedFileName = _options.UseUniqueFileNames
            ? $"{Guid.NewGuid()}{extension}"
            : chunkInfo.FileName;
            
        // 确保文件名安全
        storedFileName = MakeSafeFileName(storedFileName);
        
        // 构建相对路径和绝对路径
        var relativePath = Path.Combine(_options.UploadPath, storedFileName);
        var absolutePath = Path.Combine(_env.WebRootPath, relativePath);
        
        // 创建目录(如果不存在)
        var directory = Path.GetDirectoryName(absolutePath);
        EnsureDirectoryExists(directory);
        
        // 合并所有分块
        using (var destinationStream = new FileStream(absolutePath, FileMode.Create))
        {
            for (var i = 0; i < chunkInfo.TotalChunks; i++)
            {
                var chunkFilePath = Path.Combine(GetChunksPath(), $"{fileId}_{i}");
                
                if (!File.Exists(chunkFilePath))
                {
                    throw new FileNotFoundException($"Chunk file {i} is missing");
                }
                
                using (var sourceStream = new FileStream(chunkFilePath, FileMode.Open, FileAccess.Read))
                {
                    await sourceStream.CopyToAsync(destinationStream);
                }
                
                // 删除分块文件
                File.Delete(chunkFilePath);
            }
        }
        
        // 从元数据中移除分块信息
        _chunksMetadata.TryRemove(fileId, out _);
        
        // 病毒扫描(如果启用)
        if (_options.ScanForViruses && !await ScanFileForVirusesAsync(absolutePath))
        {
            // 删除有病毒的文件
            File.Delete(absolutePath);
            throw new InvalidOperationException("File may contain malware and was rejected");
        }
        
        // 调整图像大小(如果启用)
        if (_options.ResizeImages && IsImageFile(extension))
        {
            await ResizeImageAsync(absolutePath);
        }
        
        // 创建结果
        var result = new FileUploadResult
        {
            Success = true,
            FileName = chunkInfo.FileName,
            StoredFileName = storedFileName,
            RelativePath = relativePath.Replace('\\', '/'),
            AbsolutePath = absolutePath,
            ContentType = chunkInfo.ContentType,
            FileSize = chunkInfo.TotalSize
        };
        
        // 添加元数据
        result.Metadata["UploadTime"] = DateTime.UtcNow.ToString("o");
        result.Metadata["ContentType"] = chunkInfo.ContentType;
        result.Metadata["ChunksUploaded"] = chunkInfo.TotalChunks.ToString();
        
        _logger.LogInformation("Chunked file uploaded successfully: {FileName}, Stored as: {StoredFileName}, Size: {Size}KB",
            chunkInfo.FileName, storedFileName, chunkInfo.TotalSize / 1024);
            
        return result;
    }

    private string GetAbsoluteUploadPath()
    {
        return Path.Combine(_env.WebRootPath, _options.UploadPath);
    }

    private string GetChunksPath()
    {
        return Path.Combine(_env.WebRootPath, _options.TempChunkDir);
    }

    private void EnsureDirectoryExists(string path)
    {
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
    }

    private async Task<bool> ValidateFileContentAsync(IFormFile file, string extension)
    {
        // 跳过内容验证(如果无法检测)
        if (!ContentTypeMapping.TryGetValue(extension, out var expectedContentTypes))
        {
            return true;
        }
        
        // 简单验证:比对MIME类型
        if (expectedContentTypes.Contains(file.ContentType))
        {
            return true;
        }
        
        // 高级验证:检查文件头部
        using var stream = file.OpenReadStream();
        var buffer = new byte[8]; // 读取前8个字节作为文件签名
        
        if (await stream.ReadAsync(buffer, 0, buffer.Length) < buffer.Length)
        {
            return false; // 文件太小
        }
        
        // 检查文件签名
        return extension switch
        {
            ".jpg" or ".jpeg" => buffer[0] == 0xFF && buffer[1] == 0xD8, // JPEG头
            ".png" => buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47, // PNG头
            ".gif" => buffer[0] == 0x47 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x38, // GIF头
            ".pdf" => buffer[0] == 0x25 && buffer[1] == 0x50 && buffer[2] == 0x44 && buffer[3] == 0x46, // PDF头
            ".zip" => buffer[0] == 0x50 && buffer[1] == 0x4B, // ZIP头
            _ => true // 其他格式跳过验证
        };
    }

    private async Task<bool> ScanFileForVirusesAsync(string filePath)
    {
        // 简单的示例实现:调用外部病毒扫描程序
        // 在实际生产中,应使用专业杀毒引擎API
        
        if (string.IsNullOrEmpty(_options.VirusScannerPath))
        {
            return true; // 未配置扫描程序,视为安全
        }
        
        try
        {
            var processInfo = new ProcessStartInfo
            {
                FileName = _options.VirusScannerPath,
                Arguments = $"\"{filePath}\"",
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };
            
            using var process = Process.Start(processInfo);
            var output = await process.StandardOutput.ReadToEndAsync();
            await process.WaitForExitAsync();
            
            // 假设扫描程序返回0表示文件安全,非0表示检测到威胁
            return process.ExitCode == 0;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error scanning file for viruses: {FilePath}", filePath);
            return true; // 扫描失败视为安全(防止阻止上传)
        }
    }

    private async Task ResizeImageAsync(string imagePath)
    {
        // 注意:此处为简化示例,实际应使用专业图像处理库
        // 如SixLabors.ImageSharp、SkiaSharp等
        
        // 此处为占位代码,实际实现需要添加图像处理库引用
        await Task.CompletedTask;
    }

    private bool IsImageFile(string extension)
    {
        return extension == ".jpg" || extension == ".jpeg" || 
               extension == ".png" || extension == ".gif" || 
               extension == ".webp";
    }

    private string MakeSafeFileName(string fileName)
    {
        // 移除不安全字符
        var invalidChars = Path.GetInvalidFileNameChars();
        var safeName = new string(fileName.Where(c => !invalidChars.Contains(c)).ToArray());
        
        // 限制长度
        if (safeName.Length > 240)
        {
            var extension = Path.GetExtension(safeName);
            safeName = safeName.Substring(0, 240 - extension.Length) + extension;
        }
        
        return safeName;
    }

    private async Task RespondWithErrorAsync(HttpContext context, string message)
    {
        context.Response.StatusCode = 400;
        context.Response.ContentType = "application/json";
        
        await context.Response.WriteAsJsonAsync(new
        {
            success = false,
            error = message
        });
    }

    private void StartChunkCleanupTask()
    {
        // 启动后台任务以定期清理过期的分块
        Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    // 每小时检查一次过期分块
                    await Task.Delay(TimeSpan.FromHours(1));
                    
                    // 清理过期元数据
                    var expiredIds = _chunksMetadata
                        .Where(c => DateTime.UtcNow - c.Value.LastUpdated > _options.ChunkExpiration)
                        .Select(c => c.Key)
                        .ToList();
                        
                    foreach (var id in expiredIds)
                    {
                        if (_chunksMetadata.TryRemove(id, out _))
                        {
                            _logger.LogInformation("Removed expired chunk metadata for file ID: {FileId}", id);
                        }
                    }
                    
                    // 清理过期分块文件
                    var chunksDir = GetChunksPath();
                    if (Directory.Exists(chunksDir))
                    {
                        var files = Directory.GetFiles(chunksDir);
                        var now = DateTime.UtcNow;
                        
                        foreach (var file in files)
                        {
                            var fileInfo = new FileInfo(file);
                            if (now - fileInfo.LastWriteTimeUtc > _options.ChunkExpiration)
                            {
                                File.Delete(file);
                                _logger.LogInformation("Deleted expired chunk file: {FileName}", fileInfo.Name);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error cleaning up expired chunks");
                }
            }
        });
    }
}

public static class FileUploadMiddlewareExtensions
{
    public static IServiceCollection AddFileUploadService(
        this IServiceCollection services,
        Action<FileUploadOptions> configureOptions = null)
    {
        var options = new FileUploadOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseFileUpload(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FileUploadMiddleware>();
    }
    
    // 辅助方法:生成文件URL
    public static string GetUploadedFileUrl(this IUrlHelper url, string relativePath)
    {
        if (string.IsNullOrEmpty(relativePath))
        {
            return null;
        }
        
        return url.Content($"~/{relativePath.TrimStart('/')}");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718

注册方式:

// 在Startup.ConfigureServices中
services.AddFileUploadService(options => {
    options.MaxFileSize = 50 * 1024 * 1024; // 50MB
    options.MaxFiles = 20;
    options.UploadPath = "uploads/files";
    options.UseUniqueFileNames = true;
    options.ValidateFileContent = true;
    
    // 允许的文件类型
    options.AllowedExtensions.AddRange(new[] { 
        ".jpg", ".jpeg", ".png", ".gif", 
        ".pdf", ".doc", ".docx", ".xls", ".xlsx",
        ".zip", ".txt", ".csv" 
    });
    
    // 允许的MIME类型
    options.AllowedMimeTypes.AddRange(new[] {
        "image/jpeg", "image/png", "image/gif",
        "application/pdf",
        "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "text/plain", "text/csv",
        "application/zip"
    });
    
    // 启用分块上传(适用于大文件)
    options.EnableChunkUploads = true;
    options.ChunkUploadEndpoint = "/api/upload/chunk";
    options.ChunkSize = 2 * 1024 * 1024; // 2MB
    
    // 可选:启用图像处理
    options.ResizeImages = true;
    options.MaxImageWidth = 1920;
    options.MaxImageHeight = 1080;
    
    // 定义上传端点
    options.UploadEndpoints.Add("/api/upload");
    options.UploadEndpoints.Add("/admin/files/upload");
});

// 在Startup.Configure中
app.UseFileUpload();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

客户端示例:

<!-- 基本上传表单 -->
<form method="post" action="/api/upload" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <button type="submit">Upload</button>
</form>

<!-- 使用JavaScript的高级上传 -->
<script>
// 标准文件上传
function uploadFiles() {
    const fileInput = document.querySelector('input[type="file"]');
    const files = fileInput.files;
    
    if (files.length === 0) {
        alert('Please select files to upload');
        return;
    }
    
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
        formData.append('files', files[i]);
    }
    
    fetch('/api/upload', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        console.log('Upload successful:', data);
        // 处理上传结果
    })
    .catch(error => {
        console.error('Error uploading files:', error);
    });
}

// 分块上传大文件
function uploadLargeFile() {
    const fileInput = document.querySelector('input[type="file"]');
    const file = fileInput.files[0];
    
    if (!file) {
        alert('Please select a file to upload');
        return;
    }
    
    const chunkSize = 2 * 1024 * 1024; // 2MB chunks
    const fileId = generateUUID(); // 生成唯一文件ID
    const totalChunks = Math.ceil(file.size / chunkSize);
    
    // 上传进度
    let uploadedChunks = 0;
    
    for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
        const start = chunkIndex * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const chunk = file.slice(start, end);
        
        uploadChunk(chunk, fileId, chunkIndex, totalChunks, file.size, file.name)
            .then(result => {
                uploadedChunks++;
                updateProgress(uploadedChunks / totalChunks * 100);
                
                if (result.complete) {
                    console.log('Upload complete:', result.file);
                    // 处理完成的文件
                }
            })
            .catch(error => {
                console.error(`Error uploading chunk ${chunkIndex}:`, error);
            });
    }
}

function uploadChunk(chunk, fileId, chunkIndex, totalChunks, totalSize, fileName) {
    const formData = new FormData();
    formData.append('file', chunk, `chunk-${chunkIndex}`);
    formData.append('fileId', fileId);
    formData.append('chunkIndex', chunkIndex);
    formData.append('totalChunks', totalChunks);
    formData.append('totalSize', totalSize);
    formData.append('fileName', fileName);
    
    return fetch('/api/upload/chunk', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json());
}

function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

function updateProgress(percent) {
    const progressBar = document.getElementById('progressBar');
    progressBar.style.width = `${percent}%`;
    progressBar.textContent = `${Math.round(percent)}%`;
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

应用场景: 处理用户上传文件,如内容管理系统、文档共享平台、社交媒体和电子商务网站的产品图片上传。

市场集成: 与Azure Blob Storage、AWS S3或其他云存储解决方案集成,实现可扩展的文件存储;与图像处理服务(如Cloudinary)集成,实现高级图像处理。

# 14. API使用统计中间件

用途: 收集和分析API使用情况,生成使用报告,支持计费和限额管理。

实现:

public class ApiUsageOptions
{
    public bool EnableTracking { get; set; } = true;
    public bool TrackAnonymousRequests { get; set; } = true;
    public bool TrackPerEndpoint { get; set; } = true;
    public bool TrackPerUser { get; set; } = true;
    public bool TrackResponseSize { get; set; } = true;
    public bool TrackPerformance { get; set; } = true;
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public List<string> IncludedPaths { get; set; } = new List<string>();
    public string ApiKeyHeaderName { get; set; } = "X-API-Key";
    public string ApiStatsEndpoint { get; set; } = "/api/stats";
    public bool RequireAuthForStats { get; set; } = true;
    public List<string> AdminRoles { get; set; } = new List<string> { "Admin", "ApiManager" };
    public TimeSpan AggregationInterval { get; set; } = TimeSpan.FromMinutes(5);
    public int MaxConcurrentRequests { get; set; } = 100;
    public int RetentionDays { get; set; } = 90;
}

public class ApiUsageMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ApiUsageOptions _options;
    private readonly ILogger<ApiUsageMiddleware> _logger;
    private readonly ConcurrentDictionary<string, EndpointStats> _endpointStats = new ConcurrentDictionary<string, EndpointStats>();
    private readonly ConcurrentDictionary<string, UserStats> _userStats = new ConcurrentDictionary<string, UserStats>();
    private readonly ConcurrentDictionary<string, ApiKeyStats> _apiKeyStats = new ConcurrentDictionary<string, ApiKeyStats>();
    private readonly ConcurrentDictionary<DateTimeOffset, AggregatedStats> _timeStats = new ConcurrentDictionary<DateTimeOffset, AggregatedStats>();
    private readonly SemaphoreSlim _statsLock = new SemaphoreSlim(1, 1);
    private DateTimeOffset _lastAggregation = DateTimeOffset.UtcNow;

    public ApiUsageMiddleware(
        RequestDelegate next,
        ApiUsageOptions options,
        ILogger<ApiUsageMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 启动定期聚合任务
        if (_options.EnableTracking)
        {
            StartAggregationTask();
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否是统计API请求
        if (context.Request.Path.Equals(_options.ApiStatsEndpoint, StringComparison.OrdinalIgnoreCase))
        {
            await HandleStatsRequestAsync(context);
            return;
        }
        
        // 如果未启用跟踪或应该排除此路径,则跳过
        if (!ShouldTrackRequest(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 收集请求信息
        var requestInfo = CollectRequestInfo(context);
        var stopwatch = Stopwatch.StartNew();
        
        // 替换响应体以测量大小
        var originalBody = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            await _next(context);
            
            // 收集响应信息
            stopwatch.Stop();
            requestInfo.ResponseTime = stopwatch.ElapsedMilliseconds;
            requestInfo.StatusCode = context.Response.StatusCode;
            
            if (_options.TrackResponseSize)
            {
                requestInfo.ResponseSize = responseBodyStream.Length;
            }
            
            // 更新统计信息
            await UpdateStatsAsync(requestInfo);
            
            // 复制响应体到原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBody);
        }
        catch (Exception ex)
        {
            // 记录错误
            requestInfo.IsError = true;
            requestInfo.ErrorType = ex.GetType().Name;
            requestInfo.ResponseTime = stopwatch.ElapsedMilliseconds;
            
            // 更新统计信息
            await UpdateStatsAsync(requestInfo);
            
            throw;
        }
        finally
        {
            context.Response.Body = originalBody;
        }
    }

    private bool ShouldTrackRequest(HttpRequest request)
    {
        if (!_options.EnableTracking)
        {
            return false;
        }
        
        var path = request.Path.Value;
        
        // 检查排除路径
        if (_options.ExcludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        // 如果指定了包含路径,则检查是否在列表中
        if (_options.IncludedPaths.Count > 0)
        {
            return _options.IncludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
        }
        
        return true;
    }

    private RequestInfo CollectRequestInfo(HttpContext context)
    {
        var request = context.Request;
        var userId = context.User?.Identity?.IsAuthenticated == true 
            ? context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value 
            : null;
        var apiKey = request.Headers[_options.ApiKeyHeaderName].FirstOrDefault();
        
        return new RequestInfo
        {
            Timestamp = DateTimeOffset.UtcNow,
            Method = request.Method,
            Path = request.Path,
            UserId = userId,
            ApiKey = apiKey,
            ClientIp = GetClientIp(context),
            UserAgent = request.Headers["User-Agent"].ToString(),
            ContentLength = request.ContentLength ?? 0,
            IsAuthenticated = context.User?.Identity?.IsAuthenticated == true,
            TraceId = Activity.Current?.Id ?? context.TraceIdentifier
        };
    }

    private string GetClientIp(HttpContext context)
    {
        var ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        
        if (string.IsNullOrEmpty(ip))
        {
            ip = context.Connection.RemoteIpAddress?.ToString();
        }
        else
        {
            // X-Forwarded-For可能包含多个IP,取第一个
            ip = ip.Split(',')[0].Trim();
        }
        
        return ip;
    }

    private async Task UpdateStatsAsync(RequestInfo requestInfo)
    {
        // 限制并发更新
        if (Interlocked.Increment(ref _currentUpdates) > _options.MaxConcurrentRequests)
        {
            Interlocked.Decrement(ref _currentUpdates);
            _logger.LogWarning("Too many concurrent stat updates, skipping update for request: {Path}", requestInfo.Path);
            return;
        }
        
        try
        {
            // 更新端点统计
            if (_options.TrackPerEndpoint)
            {
                var endpointKey = $"{requestInfo.Method}:{requestInfo.Path}";
                var endpointStat = _endpointStats.GetOrAdd(endpointKey, _ => new EndpointStats
                {
                    Method = requestInfo.Method,
                    Path = requestInfo.Path
                });
                
                UpdateEndpointStats(endpointStat, requestInfo);
            }
            
            // 更新用户统计
            if (_options.TrackPerUser && requestInfo.IsAuthenticated && !string.IsNullOrEmpty(requestInfo.UserId))
            {
                var userStat = _userStats.GetOrAdd(requestInfo.UserId, _ => new UserStats
                {
                    UserId = requestInfo.UserId
                });
                
                UpdateUserStats(userStat, requestInfo);
            }
            
            // 更新API密钥统计
            if (!string.IsNullOrEmpty(requestInfo.ApiKey))
            {
                var apiKeyStat = _apiKeyStats.GetOrAdd(requestInfo.ApiKey, _ => new ApiKeyStats
                {
                    ApiKey = requestInfo.ApiKey
                });
                
                UpdateApiKeyStats(apiKeyStat, requestInfo);
            }
            
            // 检查是否需要聚合数据
            var now = DateTimeOffset.UtcNow;
            if ((now - _lastAggregation) >= _options.AggregationInterval)
            {
                await AggregateStatsAsync();
            }
        }
        finally
        {
            Interlocked.Decrement(ref _currentUpdates);
        }
    }
    
    private int _currentUpdates = 0;

    private void UpdateEndpointStats(EndpointStats stats, RequestInfo request)
    {
        Interlocked.Increment(ref stats.TotalRequests);
        
        if (request.IsError)
        {
            Interlocked.Increment(ref stats.ErrorCount);
        }
        
        if (request.StatusCode >= 400 && request.StatusCode < 500)
        {
            Interlocked.Increment(ref stats.ClientErrorCount);
        }
        else if (request.StatusCode >= 500)
        {
            Interlocked.Increment(ref stats.ServerErrorCount);
        }
        
        if (_options.TrackPerformance)
        {
            // 更新响应时间统计
            var responseTime = request.ResponseTime;
            Interlocked.Add(ref stats.TotalResponseTime, responseTime);
            
            // 更新最小响应时间(线程安全方式)
            var currentMin = stats.MinResponseTime;
            while (currentMin == 0 || responseTime < currentMin)
            {
                var oldValue = currentMin;
                var newValue = responseTime;
                currentMin = Interlocked.CompareExchange(ref stats.MinResponseTime, newValue, oldValue);
                
                if (currentMin == oldValue)
                    break;
            }
            
            // 更新最大响应时间
            var currentMax = stats.MaxResponseTime;
            while (responseTime > currentMax)
            {
                var oldValue = currentMax;
                var newValue = responseTime;
                currentMax = Interlocked.CompareExchange(ref stats.MaxResponseTime, newValue, oldValue);
                
                if (currentMax == oldValue)
                    break;
            }
        }
        
        if (_options.TrackResponseSize && request.ResponseSize.HasValue)
        {
            Interlocked.Add(ref stats.TotalResponseSize, request.ResponseSize.Value);
        }
        
        stats.LastAccessed = DateTimeOffset.UtcNow;
    }

    private void UpdateUserStats(UserStats stats, RequestInfo request)
    {
        Interlocked.Increment(ref stats.TotalRequests);
        
        if (request.IsError)
        {
            Interlocked.Increment(ref stats.ErrorCount);
        }
        
        if (_options.TrackResponseSize && request.ResponseSize.HasValue)
        {
            Interlocked.Add(ref stats.TotalDataTransferred, request.ResponseSize.Value + request.ContentLength);
        }
        
        stats.LastAccessed = DateTimeOffset.UtcNow;
        
        // 更新端点访问
        var endpointKey = $"{request.Method}:{request.Path}";
        stats.EndpointAccess.AddOrUpdate(
            endpointKey,
            _ => 1,
            (_, count) => count + 1);
    }

    private void UpdateApiKeyStats(ApiKeyStats stats, RequestInfo request)
    {
        Interlocked.Increment(ref stats.TotalRequests);
        
        if (request.IsError)
        {
            Interlocked.Increment(ref stats.ErrorCount);
        }
        
        if (_options.TrackResponseSize && request.ResponseSize.HasValue)
        {
            Interlocked.Add(ref stats.TotalDataTransferred, request.ResponseSize.Value + request.ContentLength);
        }
        
        stats.LastAccessed = DateTimeOffset.UtcNow;
        
        // 更新端点访问
        var endpointKey = $"{request.Method}:{request.Path}";
        stats.EndpointAccess.AddOrUpdate(
            endpointKey,
            _ => 1,
            (_, count) => count + 1);
    }

```csharp
    private async Task AggregateStatsAsync()
    {
        // 使用锁防止并发聚合
        if (!await _statsLock.WaitAsync(0))
        {
            return; // 另一个聚合操作正在进行
        }
        
        try
        {
            var now = DateTimeOffset.UtcNow;
            var timeKey = new DateTimeOffset(
                now.Year, now.Month, now.Day, now.Hour, 
                now.Minute / 5 * 5, 0, now.Offset); // 按5分钟时间段聚合
                
            _lastAggregation = now;
            
            var aggregated = new AggregatedStats
            {
                TimeSlot = timeKey,
                TotalRequests = 0,
                UniqueUsers = new HashSet<string>(),
                UniqueApiKeys = new HashSet<string>(),
                EndpointStats = new Dictionary<string, EndpointAggregatedStats>(),
                ErrorCount = 0,
                TotalResponseTime = 0,
                TotalDataTransferred = 0
            };
            
            // 聚合端点统计
            foreach (var stat in _endpointStats)
            {
                var endpointKey = stat.Key;
                var endpointStat = stat.Value;
                
                aggregated.TotalRequests += endpointStat.TotalRequests;
                aggregated.ErrorCount += endpointStat.ErrorCount;
                aggregated.TotalResponseTime += endpointStat.TotalResponseTime;
                aggregated.TotalDataTransferred += endpointStat.TotalResponseSize;
                
                // 聚合特定端点的统计
                aggregated.EndpointStats[endpointKey] = new EndpointAggregatedStats
                {
                    Method = endpointStat.Method,
                    Path = endpointStat.Path,
                    RequestCount = endpointStat.TotalRequests,
                    ErrorCount = endpointStat.ErrorCount,
                    AverageResponseTime = endpointStat.TotalRequests > 0 
                        ? endpointStat.TotalResponseTime / endpointStat.TotalRequests 
                        : 0,
                    TotalDataTransferred = endpointStat.TotalResponseSize
                };
            }
            
            // 聚合用户统计
            foreach (var stat in _userStats)
            {
                aggregated.UniqueUsers.Add(stat.Key);
            }
            
            // 聚合API密钥统计
            foreach (var stat in _apiKeyStats)
            {
                aggregated.UniqueApiKeys.Add(stat.Key);
            }
            
            // 存储聚合数据
            _timeStats[timeKey] = aggregated;
            
            // 清理旧数据
            CleanupOldStats(now);
            
            _logger.LogInformation(
                "API stats aggregated for time slot {TimeSlot}: {TotalRequests} requests, {UniqueUsers} users, {ErrorCount} errors",
                timeKey, aggregated.TotalRequests, aggregated.UniqueUsers.Count, aggregated.ErrorCount);
        }
        finally
        {
            _statsLock.Release();
        }
    }

    private void CleanupOldStats(DateTimeOffset now)
    {
        var cutoff = now.AddDays(-_options.RetentionDays);
        
        // 清理旧的时间统计
        foreach (var timeKey in _timeStats.Keys)
        {
            if (timeKey < cutoff)
            {
                _timeStats.TryRemove(timeKey, out _);
            }
        }
        
        // 清理长时间未访问的端点统计
        foreach (var key in _endpointStats.Keys)
        {
            if (_endpointStats.TryGetValue(key, out var stat) && stat.LastAccessed < cutoff)
            {
                _endpointStats.TryRemove(key, out _);
            }
        }
        
        // 清理长时间未访问的用户统计
        foreach (var key in _userStats.Keys)
        {
            if (_userStats.TryGetValue(key, out var stat) && stat.LastAccessed < cutoff)
            {
                _userStats.TryRemove(key, out _);
            }
        }
        
        // 清理长时间未访问的API密钥统计
        foreach (var key in _apiKeyStats.Keys)
        {
            if (_apiKeyStats.TryGetValue(key, out var stat) && stat.LastAccessed < cutoff)
            {
                _apiKeyStats.TryRemove(key, out _);
            }
        }
    }

    private void StartAggregationTask()
    {
        Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    await Task.Delay(_options.AggregationInterval);
                    await AggregateStatsAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error in stats aggregation task");
                }
            }
        });
    }

    private async Task HandleStatsRequestAsync(HttpContext context)
    {
        // 检查访问权限
        if (_options.RequireAuthForStats)
        {
            var isAuthorized = false;
            
            // 检查用户是否已认证
            if (context.User?.Identity?.IsAuthenticated == true)
            {
                // 检查用户是否在管理员角色中
                isAuthorized = _options.AdminRoles.Any(role => context.User.IsInRole(role));
            }
            
            if (!isAuthorized)
            {
                context.Response.StatusCode = 403; // Forbidden
                await context.Response.WriteAsJsonAsync(new { error = "Unauthorized access to API stats" });
                return;
            }
        }
        
        // 解析查询参数
        var query = context.Request.Query;
        var format = query["format"].ToString().ToLowerInvariant();
        var period = query["period"].ToString().ToLowerInvariant();
        var endpoint = query["endpoint"].ToString();
        var user = query["user"].ToString();
        var apiKey = query["apiKey"].ToString();
        
        // 准备统计数据
        var now = DateTimeOffset.UtcNow;
        var stats = new Dictionary<string, object>();
        
        // 基础摘要
        stats["summary"] = new
        {
            totalEndpoints = _endpointStats.Count,
            totalUsers = _userStats.Count,
            totalApiKeys = _apiKeyStats.Count,
            dataCollectionStarted = _lastAggregation.AddMinutes(-5).ToString("o"), // 估计开始时间
            currentTime = now.ToString("o")
        };
        
        // 根据请求的统计类型准备数据
        if (string.IsNullOrEmpty(endpoint) && string.IsNullOrEmpty(user) && string.IsNullOrEmpty(apiKey))
        {
            // 全局统计
            stats["globalStats"] = PrepareGlobalStats(period, now);
        }
        
        if (!string.IsNullOrEmpty(endpoint))
        {
            // 特定端点统计
            stats["endpointStats"] = PrepareEndpointStats(endpoint, period, now);
        }
        
        if (!string.IsNullOrEmpty(user))
        {
            // 特定用户统计
            stats["userStats"] = PrepareUserStats(user);
        }
        
        if (!string.IsNullOrEmpty(apiKey))
        {
            // 特定API密钥统计
            stats["apiKeyStats"] = PrepareApiKeyStats(apiKey);
        }
        
        // 设置响应
        context.Response.ContentType = format == "csv" ? "text/csv" : "application/json";
        
        if (format == "csv")
        {
            await WriteCsvResponseAsync(context.Response, stats);
        }
        else
        {
            await context.Response.WriteAsJsonAsync(stats);
        }
    }

    private object PrepareGlobalStats(string period, DateTimeOffset now)
    {
        var cutoff = GetPeriodCutoff(period, now);
        
        // 获取指定时间段内的统计数据
        var timeStats = _timeStats
            .Where(kvp => kvp.Key >= cutoff)
            .OrderBy(kvp => kvp.Key)
            .ToList();
            
        var totalRequests = timeStats.Sum(kvp => kvp.Value.TotalRequests);
        var totalErrors = timeStats.Sum(kvp => kvp.Value.ErrorCount);
        var totalResponseTime = timeStats.Sum(kvp => kvp.Value.TotalResponseTime);
        var totalDataTransferred = timeStats.Sum(kvp => kvp.Value.TotalDataTransferred);
        
        // 计算所有端点的平均响应时间
        var avgResponseTime = totalRequests > 0 ? (double)totalResponseTime / totalRequests : 0;
        
        // 准备时间序列数据
        var timeSeries = timeStats.Select(kvp => new
        {
            timeSlot = kvp.Key.ToString("o"),
            requests = kvp.Value.TotalRequests,
            errors = kvp.Value.ErrorCount,
            uniqueUsers = kvp.Value.UniqueUsers.Count,
            uniqueApiKeys = kvp.Value.UniqueApiKeys.Count,
            dataTransferred = FormatBytes(kvp.Value.TotalDataTransferred)
        }).ToList();
        
        // 获取最受欢迎的端点
        var popularEndpoints = _endpointStats
            .OrderByDescending(kvp => kvp.Value.TotalRequests)
            .Take(10)
            .Select(kvp => new
            {
                endpoint = $"{kvp.Value.Method} {kvp.Value.Path}",
                requests = kvp.Value.TotalRequests,
                errors = kvp.Value.ErrorCount,
                avgResponseTime = kvp.Value.TotalRequests > 0 
                    ? (double)kvp.Value.TotalResponseTime / kvp.Value.TotalRequests 
                    : 0,
                dataTransferred = FormatBytes(kvp.Value.TotalResponseSize)
            })
            .ToList();
            
        return new
        {
            period = period,
            totalRequests,
            totalErrors,
            errorRate = totalRequests > 0 ? (double)totalErrors / totalRequests * 100 : 0,
            avgResponseTime,
            totalDataTransferred = FormatBytes(totalDataTransferred),
            uniqueUsers = timeStats.SelectMany(kvp => kvp.Value.UniqueUsers).Distinct().Count(),
            uniqueApiKeys = timeStats.SelectMany(kvp => kvp.Value.UniqueApiKeys).Distinct().Count(),
            timeSeries,
            popularEndpoints
        };
    }

    private object PrepareEndpointStats(string endpoint, string period, DateTimeOffset now)
    {
        var cutoff = GetPeriodCutoff(period, now);
        
        // 查找指定端点的统计数据
        if (!_endpointStats.TryGetValue(endpoint, out var endpointStat))
        {
            return new { error = "Endpoint not found" };
        }
        
        // 获取指定时间段内端点的统计数据
        var timeStats = _timeStats
            .Where(kvp => kvp.Key >= cutoff && kvp.Value.EndpointStats.ContainsKey(endpoint))
            .OrderBy(kvp => kvp.Key)
            .ToList();
            
        var timeSeries = timeStats.Select(kvp => new
        {
            timeSlot = kvp.Key.ToString("o"),
            requests = kvp.Value.EndpointStats[endpoint].RequestCount,
            errors = kvp.Value.EndpointStats[endpoint].ErrorCount,
            avgResponseTime = kvp.Value.EndpointStats[endpoint].AverageResponseTime,
            dataTransferred = FormatBytes(kvp.Value.EndpointStats[endpoint].TotalDataTransferred)
        }).ToList();
        
        return new
        {
            endpoint = $"{endpointStat.Method} {endpointStat.Path}",
            period = period,
            totalRequests = endpointStat.TotalRequests,
            totalErrors = endpointStat.ErrorCount,
            errorRate = endpointStat.TotalRequests > 0 
                ? (double)endpointStat.ErrorCount / endpointStat.TotalRequests * 100 
                : 0,
            avgResponseTime = endpointStat.TotalRequests > 0 
                ? (double)endpointStat.TotalResponseTime / endpointStat.TotalRequests 
                : 0,
            minResponseTime = endpointStat.MinResponseTime,
            maxResponseTime = endpointStat.MaxResponseTime,
            totalDataTransferred = FormatBytes(endpointStat.TotalResponseSize),
            timeSeries
        };
    }

    private object PrepareUserStats(string userId)
    {
        // 查找指定用户的统计数据
        if (!_userStats.TryGetValue(userId, out var userStat))
        {
            return new { error = "User not found" };
        }
        
        // 准备用户访问的端点统计
        var endpointAccess = userStat.EndpointAccess
            .OrderByDescending(kvp => kvp.Value)
            .Take(10)
            .Select(kvp => new
            {
                endpoint = kvp.Key,
                requests = kvp.Value
            })
            .ToList();
            
        return new
        {
            userId = userStat.UserId,
            totalRequests = userStat.TotalRequests,
            totalErrors = userStat.ErrorCount,
            errorRate = userStat.TotalRequests > 0 
                ? (double)userStat.ErrorCount / userStat.TotalRequests * 100 
                : 0,
            totalDataTransferred = FormatBytes(userStat.TotalDataTransferred),
            lastAccessed = userStat.LastAccessed.ToString("o"),
            mostAccessedEndpoints = endpointAccess
        };
    }

    private object PrepareApiKeyStats(string apiKey)
    {
        // 查找指定API密钥的统计数据
        if (!_apiKeyStats.TryGetValue(apiKey, out var apiKeyStat))
        {
            return new { error = "API key not found" };
        }
        
        // 准备API密钥访问的端点统计
        var endpointAccess = apiKeyStat.EndpointAccess
            .OrderByDescending(kvp => kvp.Value)
            .Take(10)
            .Select(kvp => new
            {
                endpoint = kvp.Key,
                requests = kvp.Value
            })
            .ToList();
            
        return new
        {
            apiKey = MaskApiKey(apiKeyStat.ApiKey),
            totalRequests = apiKeyStat.TotalRequests,
            totalErrors = apiKeyStat.ErrorCount,
            errorRate = apiKeyStat.TotalRequests > 0 
                ? (double)apiKeyStat.ErrorCount / apiKeyStat.TotalRequests * 100 
                : 0,
            totalDataTransferred = FormatBytes(apiKeyStat.TotalDataTransferred),
            lastAccessed = apiKeyStat.LastAccessed.ToString("o"),
            mostAccessedEndpoints = endpointAccess
        };
    }

    private DateTimeOffset GetPeriodCutoff(string period, DateTimeOffset now)
    {
        return period switch
        {
            "hour" => now.AddHours(-1),
            "day" => now.AddDays(-1),
            "week" => now.AddDays(-7),
            "month" => now.AddMonths(-1),
            _ => now.AddDays(-1) // 默认为1天
        };
    }

    private string FormatBytes(long bytes)
    {
        string[] suffix = { "B", "KB", "MB", "GB", "TB" };
        int i;
        double dblBytes = bytes;
        
        for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024)
        {
            dblBytes = bytes / 1024.0;
        }
        
        return $"{dblBytes:0.##} {suffix[i]}";
    }

    private string MaskApiKey(string apiKey)
    {
        if (string.IsNullOrEmpty(apiKey) || apiKey.Length <= 8)
        {
            return apiKey;
        }
        
        return apiKey.Substring(0, 4) + "..." + apiKey.Substring(apiKey.Length - 4);
    }

    private Task WriteCsvResponseAsync(HttpResponse response, Dictionary<string, object> stats)
    {
        // 简化实现:只支持全局统计的CSV导出
        if (!stats.ContainsKey("globalStats"))
        {
            return response.WriteAsync("No data available for CSV export");
        }
        
        var globalStats = (dynamic)stats["globalStats"];
        var timeSeries = (IEnumerable<dynamic>)globalStats.timeSeries;
        
        var sb = new StringBuilder();
        
        // 添加CSV头
        sb.AppendLine("TimeSlot,Requests,Errors,UniqueUsers,UniqueApiKeys,DataTransferred");
        
        // 添加数据行
        foreach (var item in timeSeries)
        {
            sb.AppendLine($"{item.timeSlot},{item.requests},{item.errors},{item.uniqueUsers},{item.uniqueApiKeys},{item.dataTransferred}");
        }
        
        return response.WriteAsync(sb.ToString());
    }

    // 数据模型
    private class RequestInfo
    {
        public DateTimeOffset Timestamp { get; set; }
        public string Method { get; set; }
        public string Path { get; set; }
        public string UserId { get; set; }
        public string ApiKey { get; set; }
        public string ClientIp { get; set; }
        public string UserAgent { get; set; }
        public long ContentLength { get; set; }
        public bool IsAuthenticated { get; set; }
        public int StatusCode { get; set; }
        public long ResponseTime { get; set; }
        public long? ResponseSize { get; set; }
        public bool IsError { get; set; }
        public string ErrorType { get; set; }
        public string TraceId { get; set; }
    }

    private class EndpointStats
    {
        public string Method { get; set; }
        public string Path { get; set; }
        public long TotalRequests { get; set; }
        public long ErrorCount { get; set; }
        public long ClientErrorCount { get; set; }
        public long ServerErrorCount { get; set; }
        public long TotalResponseTime { get; set; }
        public long MinResponseTime { get; set; }
        public long MaxResponseTime { get; set; }
        public long TotalResponseSize { get; set; }
        public DateTimeOffset LastAccessed { get; set; } = DateTimeOffset.UtcNow;
    }

    private class UserStats
    {
        public string UserId { get; set; }
        public long TotalRequests { get; set; }
        public long ErrorCount { get; set; }
        public long TotalDataTransferred { get; set; }
        public ConcurrentDictionary<string, int> EndpointAccess { get; } = new ConcurrentDictionary<string, int>();
        public DateTimeOffset LastAccessed { get; set; } = DateTimeOffset.UtcNow;
    }

    private class ApiKeyStats
    {
        public string ApiKey { get; set; }
        public long TotalRequests { get; set; }
        public long ErrorCount { get; set; }
        public long TotalDataTransferred { get; set; }
        public ConcurrentDictionary<string, int> EndpointAccess { get; } = new ConcurrentDictionary<string, int>();
        public DateTimeOffset LastAccessed { get; set; } = DateTimeOffset.UtcNow;
    }

    private class EndpointAggregatedStats
    {
        public string Method { get; set; }
        public string Path { get; set; }
        public long RequestCount { get; set; }
        public long ErrorCount { get; set; }
        public double AverageResponseTime { get; set; }
        public long TotalDataTransferred { get; set; }
    }

    private class AggregatedStats
    {
        public DateTimeOffset TimeSlot { get; set; }
        public long TotalRequests { get; set; }
        public HashSet<string> UniqueUsers { get; set; }
        public HashSet<string> UniqueApiKeys { get; set; }
        public Dictionary<string, EndpointAggregatedStats> EndpointStats { get; set; }
        public long ErrorCount { get; set; }
        public long TotalResponseTime { get; set; }
        public long TotalDataTransferred { get; set; }
    }
}

public static class ApiUsageMiddlewareExtensions
{
    public static IServiceCollection AddApiUsageTracking(
        this IServiceCollection services,
        Action<ApiUsageOptions> configureOptions = null)
    {
        var options = new ApiUsageOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseApiUsageTracking(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ApiUsageMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895

注册方式:

// 在Startup.ConfigureServices中
services.AddApiUsageTracking(options => {
    options.EnableTracking = true;
    options.TrackPerEndpoint = true;
    options.TrackPerUser = true;
    options.TrackResponseSize = true;
    options.TrackPerformance = true;
    
    options.ExcludedPaths.Add("/api/stats");
    options.ExcludedPaths.Add("/health");
    options.ExcludedPaths.Add("/metrics");
    
    options.ApiStatsEndpoint = "/api/stats";
    options.RequireAuthForStats = true;
    options.AdminRoles.Add("ApiAdmin");
    
    options.AggregationInterval = TimeSpan.FromMinutes(5);
    options.RetentionDays = 30;
});

// 在Startup.Configure中
app.UseApiUsageTracking();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

查看统计的方式:

GET /api/stats?period=day
GET /api/stats?period=week&endpoint=/api/products
GET /api/stats?user=user123
GET /api/stats?apiKey=sk_test_12345&period=month
GET /api/stats?format=csv&period=day
1
2
3
4
5

应用场景: 监控API使用情况,跟踪用户行为,实现API计费和配额管理。特别适用于提供API服务的企业和SaaS平台。

市场集成: 与计费系统集成,实现基于使用量的计费;与监控系统集成,创建API使用仪表板;与API网关集成,实现高级API管理。

# 15. 国际地址格式化中间件

用途: 根据不同国家/地区的规则格式化和验证邮政地址,提高地址输入准确性。

实现:

public class AddressFormatterOptions
{
    public bool EnableFormatting { get; set; } = true;
    public bool EnableValidation { get; set; } = true;
    public string DefaultCountry { get; set; } = "US";
    public string AddressEndpoint { get; set; } = "/api/address";
    public List<string> SupportedCountries { get; set; } = new List<string>();
    public List<string> RequiredFields { get; set; } = new List<string> { "country", "addressLine1", "city", "postalCode" };
    public Dictionary<string, CountryAddressFormat> CountryFormats { get; set; } = new Dictionary<string, CountryAddressFormat>();
    public bool UseExternalService { get; set; } = false;
    public string ExternalServiceUrl { get; set; }
    public string ExternalServiceApiKey { get; set; }
}

public class CountryAddressFormat
{
    public string Country { get; set; }
    public string FormatTemplate { get; set; }
    public List<string> RequiredFields { get; set; } = new List<string>();
    public Dictionary<string, string> FieldLabels { get; set; } = new Dictionary<string, string>();
    public Dictionary<string, string> FieldValidationPatterns { get; set; } = new Dictionary<string, string>();
    public Dictionary<string, List<string>> StateOptions { get; set; } = new Dictionary<string, List<string>>();
    public List<string> PostalCodeExamples { get; set; } = new List<string>();
    public string PostalCodePattern { get; set; }
    public bool HasStates { get; set; }
    public string AddressLine1Label { get; set; } = "Street Address";
    public string AddressLine2Label { get; set; } = "Apt, Suite, etc.";
    public string CityLabel { get; set; } = "City";
    public string StateLabel { get; set; } = "State/Province";
    public string PostalCodeLabel { get; set; } = "Postal Code";
}

public class AddressModel
{
    public string Country { get; set; }
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string PostalCode { get; set; }
    public Dictionary<string, string> AdditionalFields { get; set; } = new Dictionary<string, string>();
}

public class AddressValidationResult
{
    public bool IsValid { get; set; }
    public List<string> Errors { get; set; } = new List<string>();
    public List<string> Warnings { get; set; } = new List<string>();
    public AddressModel FormattedAddress { get; set; }
    public AddressModel SuggestedAddress { get; set; }
    public CountryAddressFormat CountryFormat { get; set; }
}

public class AddressFormatterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AddressFormatterOptions _options;
    private readonly ILogger<AddressFormatterMiddleware> _logger;
    private readonly HttpClient _httpClient;

    public AddressFormatterMiddleware(
        RequestDelegate next,
        AddressFormatterOptions options,
        ILogger<AddressFormatterMiddleware> logger,
        IHttpClientFactory httpClientFactory)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient("AddressFormatter");
        
        // 初始化支持的国家
        InitializeCountryFormats();
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否是地址处理请求
        if (IsAddressEndpoint(context.Request))
        {
            await HandleAddressRequestAsync(context);
            return;
        }
        
        await _next(context);
    }

    private bool IsAddressEndpoint(HttpRequest request)
    {
        return request.Path.Equals(_options.AddressEndpoint, StringComparison.OrdinalIgnoreCase) &&
               (HttpMethods.IsPost(request.Method) || HttpMethods.IsGet(request.Method));
    }

    private async Task HandleAddressRequestAsync(HttpContext context)
    {
        if (HttpMethods.IsGet(context.Request.Method))
        {
            await HandleGetAddressFormatsAsync(context);
            return;
        }
        
        if (HttpMethods.IsPost(context.Request.Method))
        {
            await HandleFormatAddressAsync(context);
            return;
        }
        
        context.Response.StatusCode = 405; // Method Not Allowed
    }

    private async Task HandleGetAddressFormatsAsync(HttpContext context)
    {
        var country = context.Request.Query["country"].ToString();
        
        if (string.IsNullOrEmpty(country))
        {
            // 返回所有支持的国家和格式
            var supportedFormats = _options.CountryFormats
                .Select(kvp => new
                {
                    country = kvp.Key,
                    format = kvp.Value
                })
                .ToList();
                
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(new
            {
                supportedCountries = _options.SupportedCountries,
                defaultCountry = _options.DefaultCountry,
                formats = supportedFormats
            });
            
            return;
        }
        
        // 返回特定国家的格式
        country = country.ToUpperInvariant();
        
        if (!_options.CountryFormats.TryGetValue(country, out var format))
        {
            context.Response.StatusCode = 404;
            await context.Response.WriteAsJsonAsync(new
            {
                error = $"Address format for country '{country}' not found"
            });
            return;
        }
        
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(format);
    }

    private async Task HandleFormatAddressAsync(HttpContext context)
    {
        AddressModel address;
        
        try
        {
            address = await JsonSerializer.DeserializeAsync<AddressModel>(
                context.Request.Body,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error deserializing address");
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Invalid address data"
            });
            return;
        }
        
        if (address == null)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Address data is required"
            });
            return;
        }
        
        // 如果未指定国家,使用默认国家
        if (string.IsNullOrEmpty(address.Country))
        {
            address.Country = _options.DefaultCountry;
        }
        
        // 获取国家格式
        var country = address.Country.ToUpperInvariant();
        
        if (!_options.CountryFormats.TryGetValue(country, out var countryFormat))
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new
            {
                error = $"Unsupported country: {country}"
            });
            return;
        }
        
        var result = new AddressValidationResult
        {
            CountryFormat = countryFormat
        };
        
        // 验证地址
        if (_options.EnableValidation)
        {
            ValidateAddress(address, countryFormat, result);
        }
        
        // 格式化地址
        if (_options.EnableFormatting)
        {
            result.FormattedAddress = FormatAddress(address, countryFormat);
        }
        else
        {
            result.FormattedAddress = address;
        }
        
        // 如果配置为使用外部服务,调用外部服务验证地址
        if (_options.UseExternalService && _options.EnableValidation)
        {
            try
            {
                var externalResult = await ValidateWithExternalServiceAsync(address);
                
                if (externalResult != null)
                {
                    // 合并外部验证结果
                    result.IsValid = externalResult.IsValid && result.IsValid;
                    
                    if (externalResult.Errors?.Count > 0)
                    {
                        result.Errors.AddRange(externalResult.Errors);
                    }
                    
                    if (externalResult.Warnings?.Count > 0)
                    {
                        result.Warnings.AddRange(externalResult.Warnings);
                    }
                    
                    if (externalResult.SuggestedAddress != null)
                    {
                        result.SuggestedAddress = externalResult.SuggestedAddress;
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error validating address with external service");
                result.Warnings.Add("Could not validate address with external service");
            }
        }
        
        // 如果没有错误,则认为地址有效
        if (result.Errors.Count == 0 && !result.IsValid)
        {
            result.IsValid = true;
        }
        
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(result);
    }

    private void ValidateAddress(AddressModel address, CountryAddressFormat format, AddressValidationResult result)
    {
        // 验证必填字段
        foreach (var field in format.RequiredFields)
        {
            var value = GetAddressField(address, field);
            
            if (string.IsNullOrWhiteSpace(value))
            {
                result.Errors.Add($"Field '{field}' is required for {format.Country}");
                result.IsValid = false;
            }
        }
        
        // 验证邮政编码格式
        if (!string.IsNullOrEmpty(address.PostalCode) && !string.IsNullOrEmpty(format.PostalCodePattern))
        {
            if (!Regex.IsMatch(address.PostalCode, format.PostalCodePattern))
            {
                result.Errors.Add($"Invalid postal code format for {format.Country}. Example: {string.Join(", ", format.PostalCodeExamples)}");
                result.IsValid = false;
            }
        }
        
        // 验证州/省
        if (format.HasStates && !string.IsNullOrEmpty(address.State))
        {
            if (format.StateOptions.Count > 0 && !format.StateOptions.Keys.Contains(address.State))
            {
                result.Errors.Add($"Invalid state/province for {format.Country}: {address.State}");
                result.IsValid = false;
            }
        }
        
        // 验证其他字段格式
        foreach (var pattern in format.FieldValidationPatterns)
        {
            var field = pattern.Key;
            var regex = pattern.Value;
            var value = GetAddressField(address, field);
            
            if (!string.IsNullOrEmpty(value) && !Regex.IsMatch(value, regex))
            {
                result.Errors.Add($"Invalid format for field '{field}'");
                result.IsValid = false;
            }
        }
    }

    private AddressModel FormatAddress(AddressModel address, CountryAddressFormat format)
    {
        // 创建格式化后的地址副本
        var formatted = new AddressModel
        {
            Country = address.Country,
            AddressLine1 = address.AddressLine1,
            AddressLine2 = address.AddressLine2,
            City = address.City,
            State = address.State,
            PostalCode = address.PostalCode,
            AdditionalFields = new Dictionary<string, string>(address.AdditionalFields ?? new Dictionary<string, string>())
        };
        
        // 根据国家规则格式化各个字段
        
        // 格式化邮政编码
        if (!string.IsNullOrEmpty(formatted.PostalCode))
        {
            formatted.PostalCode = FormatPostalCode(formatted.PostalCode, format);
        }
        
        // 格式化州/省
        if (!string.IsNullOrEmpty(formatted.State))
        {
            formatted.State = FormatState(formatted.State, format);
        }
        
        // 格式化城市
        if (!string.IsNullOrEmpty(formatted.City))
        {
            formatted.City = FormatCity(formatted.City, format);
        }
        
        return formatted;
    }

    private string FormatPostalCode(string postalCode, CountryAddressFormat format)
    {
        // 根据国家格式化邮政编码
        switch (format.Country)
        {
            case "US":
                // 美国邮政编码格式:12345 或 12345-6789
                if (Regex.IsMatch(postalCode, @"^\d{5}$") || Regex.IsMatch(postalCode, @"^\d{5}-\d{4}$"))
                {
                    return postalCode;
                }
                
                // 如果只有9位数字,添加连字符
                if (Regex.IsMatch(postalCode, @"^\d{9}$"))
                {
                    return postalCode.Substring(0, 5) + "-" + postalCode.Substring(5);
                }
                
                // 去除非数字字符
                var digits = new string(postalCode.Where(char.IsDigit).ToArray());
                
                if (digits.Length >= 5)
                {
                    if (digits.Length > 5)
                    {
                        return digits.Substring(0, 5) + "-" + digits.Substring(5, Math.Min(4, digits.Length - 5));
                    }
                    return digits;
                }
                
                return postalCode;
                
            case "CA":
                // 加拿大邮政编码格式:A1A 1A1
                if (Regex.IsMatch(postalCode, @"^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$", RegexOptions.IgnoreCase))
                {
                    return postalCode.ToUpperInvariant();
                }
                
                // 去除空格和连字符,然后重新格式化
                var cleaned = postalCode.Replace(" ", "").Replace("-", "");
                
                if (Regex.IsMatch(cleaned, @"^[ABCEGHJKLMNPRSTVXY]\d[A-Z]\d[A-Z]\d$", RegexOptions.IgnoreCase))
                {
                    return $"{cleaned.Substring(0, 3)} {cleaned.Substring(3)}".ToUpperInvariant();
                }
                
                return postalCode;
                
            case "GB":
                // 英国邮政编码格式:多种可能,基本格式化为大写并确保有空格
                return postalCode.ToUpperInvariant();
                
            default:
                return postalCode;
        }
    }

    private string FormatState(string state, CountryAddressFormat format)
    {
        // 根据国家格式化州/省
        switch (format.Country)
        {
            case "US":
                // 美国州缩写格式化为大写
                if (state.Length == 2)
                {
                    return state.ToUpperInvariant();
                }
                
                // 尝试将州名转换为州缩写
                var stateDict = format.StateOptions;
                
                foreach (var kvp in stateDict)
                {
                    if (kvp.Value.Any(v => v.Equals(state, StringComparison.OrdinalIgnoreCase)))
                    {
                        return kvp.Key;
                    }
                }
                
                return state;
                
            case "CA":
                // 加拿大省缩写格式化为大写
                if (state.Length == 2)
                {
                    return state.ToUpperInvariant();
                }
                
                // 尝试将省名转换为省缩写
                var provinceDict = format.StateOptions;
                
                foreach (var kvp in provinceDict)
                {
                    if (kvp.Value.Any(v => v.Equals(state, StringComparison.OrdinalIgnoreCase)))
                    {
                        return kvp.Key;
                    }
                }
                
                return state;
                
            default:
                return state;
        }
    }

    private string FormatCity(string city, CountryAddressFormat format)
    {
        // 默认城市格式化(首字母大写)
        if (string.IsNullOrEmpty(city))
        {
            return city;
        }
        
        // 分词并每个单词首字母大写
        var textInfo = new CultureInfo("en-US", false).TextInfo;
        return textInfo.ToTitleCase(city.ToLowerInvariant());
    }

    private async Task<AddressValidationResult> ValidateWithExternalServiceAsync(AddressModel address)
    {
        if (string.IsNullOrEmpty(_options.ExternalServiceUrl))
        {
            return null;
        }
        
        try
        {
            var request = new HttpRequestMessage(HttpMethod.Post, _options.ExternalServiceUrl);
            
            // 添加API密钥(如果有)
            if (!string.IsNullOrEmpty(_options.ExternalServiceApiKey))
            {
                request.Headers.Add("X-API-Key", _options.ExternalServiceApiKey);
            }
            
            // 序列化地址
            var content = JsonSerializer.Serialize(address);
            request.Content = new StringContent(content, Encoding.UTF8, "application/json");
            
            // 发送请求
            var response = await _httpClient.SendAsync(request);
            
            if (!response.IsSuccessStatusCode)
            {
                _logger.LogWarning("External address validation service returned status code {StatusCode}", response.StatusCode);
                return null;
            }
            
            // 解析响应
            var responseContent = await response.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize<AddressValidationResult>(responseContent, new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error validating address with external service");
            return null;
        }
    }

    private string GetAddressField(AddressModel address, string field)
    {
        return field.ToLowerInvariant() switch
        {
            "country" => address.Country,
            "addressline1" => address.AddressLine1,
            "addressline2" => address.AddressLine2,
            "city" => address.City,
            "state" => address.State,
            "postalcode" => address.PostalCode,
            _ => address.AdditionalFields.TryGetValue(field, out var value) ? value : null
        };
    }

    private void InitializeCountryFormats()
    {
        // 如果没有预配置的国家格式,添加一些常见国家的默认格式
        if (_options.CountryFormats.Count == 0)
        {
            // 美国地址格式
            _options.CountryFormats["US"] = new CountryAddressFormat
            {
                Country = "US",
                FormatTemplate = "{AddressLine1}\n{AddressLine2}\n{City}, {State} {PostalCode}",
                RequiredFields = new List<string> { "addressLine1", "city", "state", "postalCode" },
                PostalCodePattern = @"^\d{5}(-\d{4})?$",
                PostalCodeExamples = new List<string> { "12345", "12345-6789" },
                HasStates = true,
                StateOptions = new Dictionary<string, List<string>>
                {
                    ["AL"] = new List<string> { "Alabama" },
                    ["AK"] = new List<string> { "Alaska" },
                    ["AZ"] = new List<string> { "Arizona" },
                    ["AR"] = new List<string> { "Arkansas" },
                    ["CA"] = new List<string> { "California" },
                    ["CO"] = new List<string> { "Colorado" },
                    // ... 其他州(在实际实现中应包含所有50个州)
                }
            };
            
            // 加拿大地址格式
            _options.CountryFormats["CA"] = new CountryAddressFormat
            {
                Country = "CA",
                FormatTemplate = "{AddressLine1}\n{AddressLine2}\n{City}, {State} {PostalCode}",
                RequiredFields = new List<string> { "addressLine1", "city", "state", "postalCode" },
                PostalCodePattern = @"^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$",
                PostalCodeExamples = new List<string> { "A1A 1A1" },
                HasStates = true,
                StateLabel = "Province",
                StateOptions = new Dictionary<string, List<string>>
                {
                    ["AB"] = new List<string> { "Alberta" },
                    ["BC"] = new List<string> { "British Columbia" },
                    ["MB"] = new List<string> { "Manitoba" },
                    // ... 其他省(在实际实现中应包含所有省份)
                }
            };
            
            // 英国地址格式
            _options.CountryFormats["GB"] = new CountryAddressFormat
            {
                Country = "GB",
                FormatTemplate = "{AddressLine1}\n{AddressLine2}\n{City}\n{PostalCode}",
                RequiredFields = new List<string> { "addressLine1", "city", "postalCode" },
                PostalCodePattern = @"^[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}$",
                PostalCodeExamples = new List<string> { "SW1A 1AA" },
                HasStates = false,
                CityLabel = "Town/City",
                PostalCodeLabel = "Postcode"
            };
            
            // 添加其他国家...
        }
        
        // 更新支持的国家列表
        _options.SupportedCountries = _options.CountryFormats.Keys.ToList();
    }
}

public static class AddressFormatterMiddlewareExtensions
{
    public static IServiceCollection AddAddressFormatter(
        this IServiceCollection services,
        Action<AddressFormatterOptions> configureOptions = null)
    {
        var options = new AddressFormatterOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        services.AddHttpClient();
        
        return services;
    }
    
    public static IApplicationBuilder UseAddressFormatter(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<AddressFormatterMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620

注册方式:

// 在Startup.ConfigureServices中
services.AddAddressFormatter(options => {
    options.EnableFormatting = true;
    options.EnableValidation = true;
    options.DefaultCountry = "US";
    options.AddressEndpoint = "/api/address";
    
    // 可选:配置外部验证服务
    options.UseExternalService = false;
    options.ExternalServiceUrl = "https://api.address-validator.com/validate";
    options.ExternalServiceApiKey = Configuration["AddressValidator:ApiKey"];
    
    // 可选:添加或修改国家格式
    options.CountryFormats["AU"] = new CountryAddressFormat
    {
        Country = "AU",
        FormatTemplate = "{AddressLine1}\n{AddressLine2}\n{City} {State} {PostalCode}",
        RequiredFields = new List<string> { "addressLine1", "city", "state", "postalCode" },
        PostalCodePattern = @"^\d{4}$",
        PostalCodeExamples = new List<string> { "2000" },
        HasStates = true,
        StateOptions = new Dictionary<string, List<string>>
        {
            ["NSW"] = new List<string> { "New South Wales" },
            ["VIC"] = new List<string> { "Victoria" },
            ["QLD"] = new List<string> { "Queensland" }
            // ... 其他州
        }
    };
});

// 在Startup.Configure中
app.UseAddressFormatter();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

使用示例:

// 客户端代码:获取国家地址格式
async function getAddressFormat(country) {
    const response = await fetch(`/api/address?country=${country}`);
    if (response.ok) {
        const format = await response.json();
        // 使用格式信息动态构建地址表单
        buildAddressForm(format);
    }
}

// 客户端代码:验证和格式化地址
async function validateAddress(address) {
    const response = await fetch('/api/address', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(address)
    });
    
    const result = await response.json();
    
    if (result.isValid) {
        // 使用格式化后的地址
        useFormattedAddress(result.formattedAddress);
    } else {
        // 显示验证错误
        showValidationErrors(result.errors);
        
        // 如果有建议的地址,显示给用户
        if (result.suggestedAddress) {
            showAddressSuggestion(result.suggestedAddress);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

应用场景: 电子商务网站的结账流程、用户注册表单、地址簿应用和物流系统,需要处理不同国家/地区的地址格式。

市场集成: 与Google地址验证API、SmartyStreets或其他地址验证服务集成,提供高级地址验证和自动补全功能。

# 16. 内容协商中间件

用途: 基于客户端请求的Accept头、URL扩展名或查询参数,提供多种数据格式(JSON, XML, CSV等)。

实现:

public class ContentNegotiationOptions
{
    public Dictionary<string, string> SupportedMediaTypes { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        [".json"] = "application/json",
        [".xml"] = "application/xml",
        [".csv"] = "text/csv",
        [".txt"] = "text/plain",
        [".html"] = "text/html"
    };
    
    public string DefaultMediaType { get; set; } = "application/json";
    public bool UseAcceptHeader { get; set; } = true;
    public bool UseUrlExtension { get; set; } = true;
    public bool UseQueryParameter { get; set; } = true;
    public string FormatQueryParameter { get; set; } = "format";
    public List<string> IncludedPaths { get; set; } = new List<string>();
    public List<string> ExcludedPaths { get; set; } = new List<string>();
}

public class ContentNegotiationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ContentNegotiationOptions _options;
    private readonly ILogger<ContentNegotiationMiddleware> _logger;

    public ContentNegotiationMiddleware(
        RequestDelegate next,
        ContentNegotiationOptions options,
        ILogger<ContentNegotiationMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该应用内容协商
        if (!ShouldApplyNegotiation(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 保存原始路径以便在处理后恢复
        var originalPath = context.Request.Path;
        var originalQueryString = context.Request.QueryString;
        
        // 尝试确定媒体类型
        var (mediaType, formatExtension) = DetermineMediaType(context.Request);
        
        // 如果通过URL扩展名确定了媒体类型,修改请求路径
        if (_options.UseUrlExtension && !string.IsNullOrEmpty(formatExtension))
        {
            var path = context.Request.Path.Value;
            if (path.EndsWith(formatExtension, StringComparison.OrdinalIgnoreCase))
            {
                var newPath = path.Substring(0, path.Length - formatExtension.Length);
                context.Request.Path = new PathString(newPath);
            }
        }
        
        // 如果通过查询参数确定了媒体类型,修改查询字符串
        if (_options.UseQueryParameter && context.Request.Query.ContainsKey(_options.FormatQueryParameter))
        {
            var query = context.Request.QueryString.Value;
            var formatParam = _options.FormatQueryParameter;
            var format = context.Request.Query[formatParam].ToString();
            
            // 从查询字符串中移除格式参数
            var regex = new Regex($"[?&]{formatParam}={format}(&|$)");
            var newQuery = regex.Replace(query, match => match.Value.EndsWith("&") ? "?" : "");
            
            if (newQuery == query)
            {
                // 正则表达式可能未匹配,尝试简单替换
                newQuery = query.Replace($"?{formatParam}={format}", "")
                                .Replace($"&{formatParam}={format}", "");
            }
            
            context.Request.QueryString = new QueryString(newQuery);
        }
        
        // 创建自定义响应格式化程序
        var originalBody = context.Response.Body;
        using var memoryStream = new MemoryStream();
        context.Response.Body = memoryStream;
        
        // 存储协商的媒体类型,以便在响应中使用
        context.Items["NegotiatedMediaType"] = mediaType;
        
        try
        {
            // 设置内容类型协商处理程序
            context.Response.OnStarting(() => 
            {
                // 只有在内容类型未被明确设置时才设置协商的类型
                if (string.IsNullOrEmpty(context.Response.ContentType))
                {
                    context.Response.ContentType = mediaType;
                }
                
                return Task.CompletedTask;
            });
            
            await _next(context);
            
            // 处理响应内容
            if (context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)
            {
                await FormatResponseAsync(context, mediaType, memoryStream, originalBody);
            }
            else
            {
                // 如果不是成功响应,直接复制内容
                memoryStream.Position = 0;
                await memoryStream.CopyToAsync(originalBody);
            }
        }
        finally
        {
            // 恢复原始路径和查询字符串
            context.Request.Path = originalPath;
            context.Request.QueryString = originalQueryString;
            
            // 恢复原始响应流
            context.Response.Body = originalBody;
        }
    }

    private bool ShouldApplyNegotiation(HttpRequest request)
    {
        var path = request.Path.Value;
        
        // 检查包含路径
        if (_options.IncludedPaths.Count > 0 && 
            !_options.IncludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        // 检查排除路径
        if (_options.ExcludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        return true;
    }

    private (string MediaType, string FormatExtension) DetermineMediaType(HttpRequest request)
    {
        string mediaType = null;
        string formatExtension = null;
        
        // 1. 从URL扩展名确定
        if (_options.UseUrlExtension)
        {
            formatExtension = Path.GetExtension(request.Path.Value).ToLowerInvariant();
            if (!string.IsNullOrEmpty(formatExtension) && _options.SupportedMediaTypes.TryGetValue(formatExtension, out var extMediaType))
            {
                mediaType = extMediaType;
                return (mediaType, formatExtension);
            }
        }
        
        // 2. 从查询参数确定
        if (_options.UseQueryParameter && request.Query.TryGetValue(_options.FormatQueryParameter, out var formatValues))
        {
            var format = formatValues.FirstOrDefault();
            if (!string.IsNullOrEmpty(format))
            {
                // 查找匹配的媒体类型
                var extension = format.StartsWith(".") ? format : $".{format}";
                if (_options.SupportedMediaTypes.TryGetValue(extension, out var queryMediaType))
                {
                    mediaType = queryMediaType;
                    return (mediaType, null);
                }
                
                // 直接检查是否是已知媒体类型名称
                if (_options.SupportedMediaTypes.ContainsValue(format))
                {
                    mediaType = format;
                    return (mediaType, null);
                }
            }
        }
        
        // 3. 从Accept头确定
        if (_options.UseAcceptHeader && request.Headers.TryGetValue("Accept", out var acceptValues))
        {
            var acceptHeader = acceptValues.ToString();
            if (!string.IsNullOrEmpty(acceptHeader))
            {
                var acceptedTypes = acceptHeader.Split(',')
                    .Select(t => t.Trim().Split(';')[0].Trim()) // 移除质量值
                    .ToList();
                
                // 检查是否有支持的媒体类型
                foreach (var acceptedType in acceptedTypes)
                {
                    if (acceptedType == "*/*")
                    {
                        // 客户端接受任何类型,使用默认
                        break;
                    }
                    
                    if (_options.SupportedMediaTypes.ContainsValue(acceptedType))
                    {
                        mediaType = acceptedType;
                        return (mediaType, null);
                    }
                }
            }
        }
        
        // 使用默认媒体类型
        return (_options.DefaultMediaType, null);
    }

    private async Task FormatResponseAsync(HttpContext context, string mediaType, MemoryStream memoryStream, Stream originalBody)
    {
        // 重置内存流位置
        memoryStream.Position = 0;
        
        // 读取内存流中的内容
        string content;
        using (var reader = new StreamReader(memoryStream, Encoding.UTF8, leaveOpen: true))
        {
            content = await reader.ReadToEndAsync();
        }
        
        // 如果内容为空,直接返回
        if (string.IsNullOrEmpty(content))
        {
            memoryStream.Position = 0;
            await memoryStream.CopyToAsync(originalBody);
            return;
        }
        
        // 如果内容类型已经与请求的媒体类型匹配,直接复制
        var currentContentType = context.Response.ContentType ?? "";
        if (currentContentType.StartsWith(mediaType, StringComparison.OrdinalIgnoreCase))
        {
            memoryStream.Position = 0;
            await memoryStream.CopyToAsync(originalBody);
            return;
        }
        
        // 尝试解析内容以转换格式
        if (currentContentType.Contains("application/json"))
        {
            // 从JSON转换到其他格式
            await ConvertFromJsonAsync(content, mediaType, context, originalBody);
        }
        else if (currentContentType.Contains("application/xml"))
        {
            // 从XML转换到其他格式
            await ConvertFromXmlAsync(content, mediaType, context, originalBody);
        }
        else
        {
            // 无法转换,直接复制
            memoryStream.Position = 0;
            await memoryStream.CopyToAsync(originalBody);
        }
    }

    private async Task ConvertFromJsonAsync(string jsonContent, string targetMediaType, HttpContext context, Stream outputStream)
    {
        try
        {
            // 解析JSON
            using var jsonDoc = JsonDocument.Parse(jsonContent);
            
            // 根据目标媒体类型转换
            if (targetMediaType == "application/xml" || targetMediaType == "text/xml")
            {
                // 将JSON转换为XML
                var xml = ConvertJsonToXml(jsonDoc.RootElement);
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(xml));
            }
            else if (targetMediaType == "text/csv")
            {
                // 将JSON转换为CSV
                var csv = ConvertJsonToCsv(jsonDoc.RootElement);
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(csv));
            }
            else if (targetMediaType == "text/plain")
            {
                // 将JSON转换为文本
                var text = jsonDoc.RootElement.ToString();
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(text));
            }
            else
            {
                // 默认保持JSON格式
                context.Response.ContentType = "application/json";
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(jsonContent));
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error converting JSON to {TargetMediaType}", targetMediaType);
            context.Response.ContentType = "application/json";
            await outputStream.WriteAsync(Encoding.UTF8.GetBytes(jsonContent));
        }
    }

    private async Task ConvertFromXmlAsync(string xmlContent, string targetMediaType, HttpContext context, Stream outputStream)
    {
        try
        {
            if (targetMediaType == "application/json")
            {
                // 将XML转换为JSON
                var json = ConvertXmlToJson(xmlContent);
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(json));
            }
            else if (targetMediaType == "text/csv")
            {
                // 将XML转换为CSV
                var csv = ConvertXmlToCsv(xmlContent);
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(csv));
            }
            else if (targetMediaType == "text/plain")
            {
                // 将XML转换为文本
                context.Response.ContentType = targetMediaType;
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(xmlContent));
            }
            else
            {
                // 默认保持XML格式
                context.Response.ContentType = "application/xml";
                await outputStream.WriteAsync(Encoding.UTF8.GetBytes(xmlContent));
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error converting XML to {TargetMediaType}", targetMediaType);
            context.Response.ContentType = "application/xml";
            await outputStream.WriteAsync(Encoding.UTF8.GetBytes(xmlContent));
        }
    }

    private string ConvertJsonToXml(JsonElement json)
    {
        var sb = new StringBuilder();
        using var writer = XmlWriter.Create(sb, new XmlWriterSettings { Indent = true });
        
        writer.WriteStartDocument();
        writer.WriteStartElement("Root");
        
        ConvertJsonElementToXml(writer, json, "Root");
        
        writer.WriteEndElement();
        writer.WriteEndDocument();
        writer.Flush();
        
        return sb.ToString();
    }

    private void ConvertJsonElementToXml(XmlWriter writer, JsonElement element, string elementName)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                foreach (var property in element.EnumerateObject())
                {
                    var propertyName = property.Name;
                    
                    // 处理特殊字符
                    if (!XmlConvert.IsStartNameChar(propertyName[0]))
                    {
                        propertyName = "_" + propertyName;
                    }
                    
                    writer.WriteStartElement(propertyName);
                    ConvertJsonElementToXml(writer, property.Value, propertyName);
                    writer.WriteEndElement();
                }
                break;
                
            case JsonValueKind.Array:
                var index = 0;
                foreach (var item in element.EnumerateArray())
                {
                    writer.WriteStartElement("Item");
                    writer.WriteAttributeString("Index", index.ToString());
                    ConvertJsonElementToXml(writer, item, "Item");
                    writer.WriteEndElement();
                    index++;
                }
                break;
                
            case JsonValueKind.String:
                writer.WriteString(element.GetString());
                break;
                
            case JsonValueKind.Number:
                writer.WriteString(element.GetRawText());
                break;
                
            case JsonValueKind.True:
                writer.WriteString("true");
                break;
                
            case JsonValueKind.False:
                writer.WriteString("false");
                break;
                
            case JsonValueKind.Null:
                writer.WriteAttributeString("xsi", "nil", "http://www.w3.org/2001/XMLSchema-instance", "true");
                break;
        }
    }

    private string ConvertJsonToCsv(JsonElement json)
    {
        if (json.ValueKind != JsonValueKind.Array)
        {
            // 如果不是数组,创建一个包含单个项目的CSV
            return ConvertSingleJsonObjectToCsv(json);
        }
        
        var array = json.EnumerateArray().ToList();
        if (array.Count == 0)
        {
            return string.Empty;
        }
        
        // 假设所有数组项具有相同的结构,使用第一项提取列
        var firstItem = array[0];
        if (firstItem.ValueKind != JsonValueKind.Object)
        {
            // 简单数组
            var sb = new StringBuilder();
            sb.AppendLine("Value");
            
            foreach (var item in array)
            {
                sb.AppendLine(GetJsonElementValue(item));
            }
            
            return sb.ToString();
        }
        
        // 对象数组
        var headers = firstItem.EnumerateObject().Select(p => p.Name).ToList();
        var csv = new StringBuilder();
        
        // 添加标题行
        csv.AppendLine(string.Join(",", headers.Select(EscapeCsvField)));
        
        // 添加数据行
        foreach (var item in array)
        {
            if (item.ValueKind != JsonValueKind.Object)
            {
                continue;
            }
            
            var values = new List<string>();
            
            foreach (var header in headers)
            {
                if (item.TryGetProperty(header, out var property))
                {
                    values.Add(EscapeCsvField(GetJsonElementValue(property)));
                }
                else
                {
                    values.Add(string.Empty);
                }
            }
            
            csv.AppendLine(string.Join(",", values));
        }
        
        return csv.ToString();
    }

    private string ConvertSingleJsonObjectToCsv(JsonElement json)
    {
        if (json.ValueKind != JsonValueKind.Object)
        {
            return json.ToString();
        }
        
        var csv = new StringBuilder();
        
        // 创建两列CSV:Key和Value
        csv.AppendLine("Key,Value");
        
        foreach (var property in json.EnumerateObject())
        {
            csv.AppendLine($"{EscapeCsvField(property.Name)},{EscapeCsvField(GetJsonElementValue(property.Value))}");
        }
        
        return csv.ToString();
    }

    private string GetJsonElementValue(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.String => element.GetString(),
            JsonValueKind.Number => element.GetRawText(),
            JsonValueKind.True => "true",
            JsonValueKind.False => "false",
            JsonValueKind.Null => "",
            _ => element.ToString()
        };
    }

    private string EscapeCsvField(string field)
    {
        if (string.IsNullOrEmpty(field))
        {
            return "";
        }
        
        bool containsSpecialChar = field.Contains(',') || field.Contains('"') || field.Contains('\n');
        
        if (containsSpecialChar)
        {
            return $"\"{field.Replace("\"", "\"\"")}\"";
        }
        
        return field;
    }

    private string ConvertXmlToJson(string xml)
    {
        var doc = new XmlDocument();
        doc.LoadXml(xml);
        
        return JsonConvert.SerializeXmlNode(doc);
    }

    private string ConvertXmlToCsv(string xml)
    {
        // 先转换为JSON,再转换为CSV
        var json = ConvertXmlToJson(xml);
        
        using var jsonDoc = JsonDocument.Parse(json);
        return ConvertJsonToCsv(jsonDoc.RootElement);
    }
}

public static class ContentNegotiationMiddlewareExtensions
{
    public static IServiceCollection AddContentNegotiation(
        this IServiceCollection services,
        Action<ContentNegotiationOptions> configureOptions = null)
    {
        var options = new ContentNegotiationOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseContentNegotiation(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ContentNegotiationMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577

注册方式:

// 在Startup.ConfigureServices中
services.AddContentNegotiation(options => {
    options.UseAcceptHeader = true;
    options.UseUrlExtension = true;
    options.UseQueryParameter = true;
    
    // 配置支持的媒体类型
    options.SupportedMediaTypes[".json"] = "application/json";
    options.SupportedMediaTypes[".xml"] = "application/xml";
    options.SupportedMediaTypes[".csv"] = "text/csv";
    options.SupportedMediaTypes[".txt"] = "text/plain";
    
    // 指定默认媒体类型
    options.DefaultMediaType = "application/json";
    
    // 配置路径
    options.IncludedPaths.Add("/api/");
    options.ExcludedPaths.Add("/api/internal/");
});

// 在Startup.Configure中
app.UseContentNegotiation();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

使用示例:

// 控制器中不需要特别处理,正常返回数据即可
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        var products = new[]
        {
            new { Id = 1, Name = "Product A", Price = 19.99 },
            new { Id = 2, Name = "Product B", Price = 29.99 },
            new { Id = 3, Name = "Product C", Price = 39.99 }
        };
        
        return Ok(products);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

客户端使用示例:

// 使用URL扩展名
GET /api/products.json  // 返回JSON
GET /api/products.xml   // 返回XML
GET /api/products.csv   // 返回CSV

// 使用查询参数
GET /api/products?format=json
GET /api/products?format=xml
GET /api/products?format=csv

// 使用Accept头
GET /api/products
Accept: application/json

GET /api/products
Accept: application/xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

应用场景: 构建多格式支持的API,允许客户端选择最合适的数据格式。适用于需要支持多种客户端(如浏览器、移动应用、传统系统)的API。

市场集成: 与API网关(如Azure API Management)集成,支持内容协商策略;与文档生成工具(如Swagger)集成,展示API支持的多种格式。

# 17. 接口规范强制中间件

用途: 确保API请求和响应符合预定义的规范,提供一致的接口体验。

实现:

public class ApiSpecOptions
{
    public bool EnforceVersioning { get; set; } = true;
    public bool EnforceConsistentHeaders { get; set; } = true;
    public bool EnforceConsistentResponse { get; set; } = true;
    public bool EnforceRequestValidation { get; set; } = true;
    public HashSet<string> RequiredRequestHeaders { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    public HashSet<string> RequiredResponseHeaders { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    public Dictionary<int, ErrorResponseTemplate> ErrorTemplates { get; set; } = new Dictionary<int, ErrorResponseTemplate>();
    public string ApiVersionHeaderName { get; set; } = "X-API-Version";
    public string CorrelationIdHeaderName { get; set; } = "X-Correlation-ID";
    public List<string> IncludedPaths { get; set; } = new List<string>();
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public string SuccessResponseTemplate { get; set; } = @"{""data"":{0},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}""}}";
    public Func<HttpContext, object, Task<object>> ResponseTransformer { get; set; }
}

public class ErrorResponseTemplate
{
    public string Template { get; set; }
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}

public class ApiSpecMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ApiSpecOptions _options;
    private readonly ILogger<ApiSpecMiddleware> _logger;

    public ApiSpecMiddleware(
        RequestDelegate next,
        ApiSpecOptions options,
        ILogger<ApiSpecMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 设置默认错误模板
        if (!_options.ErrorTemplates.ContainsKey(400))
        {
            _options.ErrorTemplates[400] = new ErrorResponseTemplate
            {
                Template = @"{""error"":{""code"":400,""message"":""Bad Request"",""details"":""{0}""},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}""}}"
            };
        }
        
        if (!_options.ErrorTemplates.ContainsKey(401))
        {
            _options.ErrorTemplates[401] = new ErrorResponseTemplate
            {
                Template = @"{""error"":{""code"":401,""message"":""Unauthorized"",""details"":""{0}""},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}""}}",
                Headers = new Dictionary<string, string>
                {
                    ["WWW-Authenticate"] = "Bearer"
                }
            };
        }
        
        if (!_options.ErrorTemplates.ContainsKey(404))
        {
            _options.ErrorTemplates[404] = new ErrorResponseTemplate
            {
                Template = @"{""error"":{""code"":404,""message"":""Not Found"",""details"":""{0}""},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}""}}"
            };
        }
        
        if (!_options.ErrorTemplates.ContainsKey(500))
        {
            _options.ErrorTemplates[500] = new ErrorResponseTemplate
            {
                Template = @"{""error"":{""code"":500,""message"":""Internal Server Error"",""details"":""{0}""},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}""}}"
            };
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该应用API规范
        if (!ShouldApplyApiSpec(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 生成关联ID(如果尚未提供)
        var correlationId = EnsureCorrelationId(context);
        
        // 验证请求头
        if (_options.EnforceConsistentHeaders && !ValidateRequestHeaders(context))
        {
            await RespondWithErrorAsync(context, 400, "Missing required headers", correlationId);
            return;
        }
        
        // 验证API版本
        if (_options.EnforceVersioning && !ValidateApiVersion(context))
        {
            await RespondWithErrorAsync(context, 400, "Invalid or missing API version", correlationId);
            return;
        }
        
        // 拦截响应以应用一致的格式
        var originalBody = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            // 处理请求
            await _next(context);
            
            // 处理响应
            await ProcessResponseAsync(context, responseBodyStream, originalBody, correlationId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception in API request");
            
            // 重置响应
            context.Response.Body = originalBody;
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            
            await RespondWithErrorAsync(context, 500, "An unexpected error occurred", correlationId);
        }
    }

    private bool ShouldApplyApiSpec(HttpRequest request)
    {
        var path = request.Path.Value;
        
        // 检查包含路径
        if (_options.IncludedPaths.Count > 0 && 
            !_options.IncludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        // 检查排除路径
        if (_options.ExcludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        return true;
    }

    private string EnsureCorrelationId(HttpContext context)
    {
        string correlationId = null;
        
        // 尝试从请求头获取关联ID
        if (context.Request.Headers.TryGetValue(_options.CorrelationIdHeaderName, out var values))
        {
            correlationId = values.FirstOrDefault();
        }
        
        // 如果没有提供,生成新的
        if (string.IsNullOrEmpty(correlationId))
        {
            correlationId = Guid.NewGuid().ToString();
        }
        
        // 确保关联ID存在于请求头中
        context.Request.Headers[_options.CorrelationIdHeaderName] = correlationId;
        
        // 确保关联ID存在于响应头中
        context.Response.OnStarting(() => 
        {
            if (!context.Response.Headers.ContainsKey(_options.CorrelationIdHeaderName))
            {
                context.Response.Headers[_options.CorrelationIdHeaderName] = correlationId;
            }
            return Task.CompletedTask;
        });
        
        return correlationId;
    }

    private bool ValidateRequestHeaders(HttpContext context)
    {
        foreach (var requiredHeader in _options.RequiredRequestHeaders)
        {
            if (!context.Request.Headers.ContainsKey(requiredHeader))
            {
                _logger.LogWarning("Request missing required header: {Header}", requiredHeader);
                return false;
            }
        }
        
        return true;
    }

    private bool ValidateApiVersion(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue(_options.ApiVersionHeaderName, out var versionValues))
        {
            _logger.LogWarning("Request missing API version header: {Header}", _options.ApiVersionHeaderName);
            return false;
        }
        
        var version = versionValues.FirstOrDefault();
        if (string.IsNullOrEmpty(version))
        {
            _logger.LogWarning("Empty API version provided");
            return false;
        }
        
        // 在真实场景中,可能需要进一步验证版本格式或检查是否支持特定版本
        
        return true;
    }

    private async Task ProcessResponseAsync(HttpContext context, MemoryStream responseBodyStream, Stream originalBody, string correlationId)
    {
        // 验证响应头
        if (_options.EnforceConsistentHeaders)
        {
            EnsureResponseHeaders(context, correlationId);
        }
        
        // 处理错误响应
        var statusCode = context.Response.StatusCode;
        if (statusCode >= 400 && _options.ErrorTemplates.ContainsKey(statusCode))
        {
            // 获取原始响应内容
            responseBodyStream.Position = 0;
            var responseContent = await new StreamReader(responseBodyStream).ReadToEndAsync();
            
            // 使用错误模板创建新响应
            var errorDetails = ExtractErrorMessage(responseContent, statusCode);
            await RespondWithErrorAsync(context, statusCode, errorDetails, correlationId, originalBody);
            return;
        }
        
        // 处理成功响应
        if (statusCode >= 200 && statusCode < 300 && _options.EnforceConsistentResponse)
        {
            await TransformSuccessResponseAsync(context, responseBodyStream, originalBody, correlationId);
            return;
        }
        
        // 对于其他响应,直接传递
        responseBodyStream.Position = 0;
        await responseBodyStream.CopyToAsync(originalBody);
    }

    private void EnsureResponseHeaders(HttpContext context, string correlationId)
    {
        // 添加所有必需的响应头
        foreach (var requiredHeader in _options.RequiredResponseHeaders)
        {
            if (!context.Response.Headers.ContainsKey(requiredHeader))
            {
                // 为特殊头部设置值
                if (requiredHeader.Equals(_options.CorrelationIdHeaderName, StringComparison.OrdinalIgnoreCase))
                {
                    context.Response.Headers[requiredHeader] = correlationId;
                }
                else if (requiredHeader.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
                {
                    context.Response.Headers[requiredHeader] = "application/json; charset=utf-8";
                }
                else
                {
                    context.Response.Headers[requiredHeader] = string.Empty;
                }
            }
        }
    }

    private string ExtractErrorMessage(string responseContent, int statusCode)
    {
        if (string.IsNullOrWhiteSpace(responseContent))
        {
            return GetDefaultErrorMessage(statusCode);
        }
        
        try
        {
            // 尝试从JSON响应中提取错误消息
            using var doc = JsonDocument.Parse(responseContent);
            
            // 首先尝试标准错误结构
            if (doc.RootElement.TryGetProperty("error", out var errorElement))
            {
                if (errorElement.TryGetProperty("message", out var messageElement) || 
                    errorElement.TryGetProperty("detail", out messageElement) ||
                    errorElement.TryGetProperty("description", out messageElement))
                {
                    return messageElement.GetString();
                }
            }
            
            // 尝试其他常见模式
            if (doc.RootElement.TryGetProperty("message", out var message))
            {
                return message.GetString();
            }
            
            if (doc.RootElement.TryGetProperty("title", out var title))
            {
                return title.GetString();
            }
            
            // 如果找不到明确的错误消息,使用整个响应
            return responseContent.Length > 100 ? responseContent.Substring(0, 100) + "..." : responseContent;
        }
        catch
        {
            // 如果解析失败,返回默认消息
            return GetDefaultErrorMessage(statusCode);
        }
    }

    private string GetDefaultErrorMessage(int statusCode)
    {
        return statusCode switch
        {
            400 => "Bad Request",
            401 => "Unauthorized",
            403 => "Forbidden",
            404 => "Not Found",
            405 => "Method Not Allowed",
            406 => "Not Acceptable",
            409 => "Conflict",
            415 => "Unsupported Media Type",
            422 => "Unprocessable Entity",
            429 => "Too Many Requests",
            500 => "Internal Server Error",
            502 => "Bad Gateway",
            503 => "Service Unavailable",
            504 => "Gateway Timeout",
            _ => "An error occurred"
        };
    }

    private async Task RespondWithErrorAsync(HttpContext context, int statusCode, string details, string correlationId, Stream outputStream = null)
    {
        if (!_options.ErrorTemplates.TryGetValue(statusCode, out var template))
        {
            // 如果没有特定状态码的模板,尝试使用默认模板
            if (!_options.ErrorTemplates.TryGetValue(500, out template))
            {
                template = new ErrorResponseTemplate
                {
                    Template = @"{""error"":{""code"":{0},""message"":""Error"",""details"":""{1}""},""meta"":{""timestamp"":""{2}"",""correlationId"":""{3}""}}"
                };
            }
        }
        
        // 设置状态码和内容类型
        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/json; charset=utf-8";
        
        // 添加自定义头部
        foreach (var header in template.Headers)
        {
            context.Response.Headers[header.Key] = header.Value;
        }
        
        // 格式化错误响应
        var timestamp = DateTime.UtcNow.ToString("o");
        var sanitizedDetails = details?.Replace("\"", "\\\"") ?? "Unknown error";
        
        var responseJson = string.Format(
            template.Template,
            sanitizedDetails,
            timestamp,
            correlationId);
        
        // 写入响应
        var responseBytes = Encoding.UTF8.GetBytes(responseJson);
        
        if (outputStream != null)
        {
            await outputStream.WriteAsync(responseBytes);
        }
        else
        {
            await context.Response.Body.WriteAsync(responseBytes);
        }
    }

    private async Task TransformSuccessResponseAsync(HttpContext context, MemoryStream responseBodyStream, Stream originalBody, string correlationId)
    {
        // 读取原始响应
        responseBodyStream.Position = 0;
        var responseContent = await new StreamReader(responseBodyStream).ReadToEndAsync();
        
        if (string.IsNullOrWhiteSpace(responseContent))
        {
            // 空响应,可以是204状态码
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBody);
            return;
        }
        
        // 解析原始数据
        object responseData;
        try
        {
            responseData = JsonSerializer.Deserialize<object>(responseContent);
        }
        catch
        {
            // 如果不是有效的JSON,直接传递
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBody);
            return;
        }
        
        // 应用自定义转换(如果配置)
        if (_options.ResponseTransformer != null)
        {
            responseData = await _options.ResponseTransformer(context, responseData);
        }
        
        // 格式化为标准响应格式
        var timestamp = DateTime.UtcNow.ToString("o");
        var formattedResponseJson = string.Format(
            _options.SuccessResponseTemplate,
            responseContent,
            timestamp,
            correlationId);
        
        // 设置内容类型
        context.Response.ContentType = "application/json; charset=utf-8";
        
        // 写入新的响应
        var responseBytes = Encoding.UTF8.GetBytes(formattedResponseJson);
        await originalBody.WriteAsync(responseBytes);
    }
}

public static class ApiSpecMiddlewareExtensions
{
    public static IServiceCollection AddApiSpecification(
        this IServiceCollection services,
        Action<ApiSpecOptions> configureOptions = null)
    {
        var options = new ApiSpecOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseApiSpecification(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ApiSpecMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455

注册方式:

// 在Startup.ConfigureServices中
services.AddApiSpecification(options => {
    options.EnforceVersioning = true;
    options.EnforceConsistentHeaders = true;
    options.EnforceConsistentResponse = true;
    
    // 定义必需的请求和响应头
    options.RequiredRequestHeaders.Add("X-API-Version");
    options.RequiredRequestHeaders.Add("Accept");
    
    options.RequiredResponseHeaders.Add("X-Correlation-ID");
    options.RequiredResponseHeaders.Add("X-API-Version");
    
    // 包含和排除路径
    options.IncludedPaths.Add("/api/v1/");
    options.IncludedPaths.Add("/api/v2/");
    options.ExcludedPaths.Add("/api/v1/internal/");
    
    // 自定义成功响应模板
    options.SuccessResponseTemplate = @"{""data"":{0},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}"",""status"":""success""}}";
    
    // 自定义错误响应模板
    options.ErrorTemplates[400] = new ErrorResponseTemplate {
        Template = @"{""error"":{""code"":400,""message"":""Bad Request"",""details"":""{0}""},""meta"":{""timestamp"":""{1}"",""correlationId"":""{2}"",""status"":""error""}}"
    };
    
    // 可选:自定义响应转换器
    options.ResponseTransformer = (context, data) => {
        // 在返回前对响应数据进行转换
        // 例如:添加分页元数据、过滤敏感数据等
        return Task.FromResult(data);
    };
});

// 在Startup.Configure中
app.UseApiSpecification();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

使用示例:

// 控制器代码不需要特别修改,框架会自动处理响应格式
[ApiController]
[Route("api/v1/users")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetUsers()
    {
        var users = new[]
        {
            new { Id = 1, Name = "John Doe", Email = "john@example.com" },
            new { Id = 2, Name = "Jane Smith", Email = "jane@example.com" }
        };
        
        return Ok(users);
    }
    
    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        if (id <= 0)
        {
            return BadRequest("Invalid user ID");
        }
        
        if (id > 100)
        {
            return NotFound("User not found");
        }
        
        var user = new { Id = id, Name = "John Doe", Email = "john@example.com" };
        return Ok(user);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

响应格式:

成功响应格式:

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com"
    },
    {
      "id": 2,
      "name": "Jane Smith",
      "email": "jane@example.com"
    }
  ],
  "meta": {
    "timestamp": "2023-05-15T12:34:56.789Z",
    "correlationId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "success"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

错误响应格式:

{
  "error": {
    "code": 404,
    "message": "Not Found",
    "details": "User not found"
  },
  "meta": {
    "timestamp": "2023-05-15T12:34:56.789Z",
    "correlationId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "error"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

应用场景: 确保企业API在不同团队和不同服务间保持一致的接口规范。特别适用于微服务架构、大型企业API和开放给第三方的API。

市场集成: 与API文档工具(如Swagger)集成,根据规范生成文档;与API网关集成,实现更高级的请求/响应转换。

# 18. 自动HTTP缓存控制中间件

用途: 智能管理HTTP缓存头,优化客户端缓存策略,减少不必要的请求。

实现:

public class HttpCacheOptions
{
    public bool EnableETag { get; set; } = true;
    public bool EnableLastModified { get; set; } = true;
    public bool EnableVaryHeader { get; set; } = true;
    public bool EnableCacheControl { get; set; } = true;
    public bool DisableCacheForAuthenticatedRequests { get; set; } = true;
    public CacheLocation DefaultCacheLocation { get; set; } = CacheLocation.Private;
    public List<string> DefaultVaryHeaders { get; set; } = new List<string> { "Accept", "Accept-Encoding", "Accept-Language" };
    public Dictionary<string, CacheProfile> CacheProfiles { get; set; } = new Dictionary<string, CacheProfile>();
    public Dictionary<string, CacheProfile> PathProfiles { get; set; } = new Dictionary<string, CacheProfile>();
    public Func<HttpContext, CacheDecision> CacheDecisionDelegate { get; set; }
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public List<string> NonCacheablePaths { get; set; } = new List<string>();
    public HashSet<string> CacheableContentTypes { get; set; } = new HashSet<string>
    {
        "text/html",
        "text/css",
        "text/javascript",
        "application/javascript",
        "text/plain",
        "application/json",
        "application/xml",
        "text/xml",
        "image/png",
        "image/jpeg",
        "image/gif",
        "image/svg+xml",
        "image/webp",
        "font/woff",
        "font/woff2"
    };
}

public enum CacheLocation
{
    Any,
    Private,
    Public,
    NoCache
}

public class CacheProfile
{
    public TimeSpan? Duration { get; set; }
    public CacheLocation Location { get; set; } = CacheLocation.Private;
    public bool NoStore { get; set; }
    public bool MustRevalidate { get; set; }
    public bool NoTransform { get; set; }
    public bool ProxyRevalidate { get; set; }
    public List<string> VaryByHeaders { get; set; } = new List<string>();
    public string ETagGenerator { get; set; } = "default";
}

public class CacheDecision
{
    public bool ShouldCache { get; set; }
    public TimeSpan? Duration { get; set; }
    public CacheLocation Location { get; set; } = CacheLocation.Private;
    public bool NoStore { get; set; }
    public bool MustRevalidate { get; set; }
    public List<string> VaryByHeaders { get; set; } = new List<string>();
}

public class HttpCacheMiddleware
{
    private readonly RequestDelegate _next;
    private readonly HttpCacheOptions _options;
    private readonly ILogger<HttpCacheMiddleware> _logger;
    private readonly Dictionary<string, Func<byte[], string>> _etagGenerators;

    public HttpCacheMiddleware(
        RequestDelegate next,
        HttpCacheOptions options,
        ILogger<HttpCacheMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
        
        // 初始化ETag生成器
        _etagGenerators = new Dictionary<string, Func<byte[], string>>
        {
            ["default"] = GenerateETagDefault,
            ["md5"] = GenerateETagMd5,
            ["sha1"] = GenerateETagSha1,
            ["sha256"] = GenerateETagSha256,
            ["murmur"] = GenerateETagMurmur
        };
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该应用缓存控制
        if (!ShouldApplyCacheControl(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 检查条件请求
        if (IsConditionalRequest(context) && await ValidateConditionalsAsync(context))
        {
            // 条件验证通过,返回304 Not Modified
            context.Response.StatusCode = StatusCodes.Status304NotModified;
            return;
        }
        
        // 拦截响应以处理缓存头
        var originalBody = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        try
        {
            // 处理请求
            await _next(context);
            
            // 只处理成功的GET或HEAD响应
            if ((context.Request.Method == "GET" || context.Request.Method == "HEAD") && 
                context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)
            {
                // 获取缓存决策
                var cacheDecision = GetCacheDecision(context);
                
                if (cacheDecision.ShouldCache)
                {
                    await ApplyCacheHeadersAsync(context, cacheDecision, responseBodyStream);
                }
                else
                {
                    // 如果不应缓存,添加禁止缓存的头部
                    AddNoCacheHeaders(context.Response);
                }
            }
            else if (context.Response.StatusCode >= 200)
            {
                // 对于非GET/HEAD请求或非成功响应,通常不缓存
                AddNoCacheHeaders(context.Response);
            }
            
            // 将响应复制到原始流
            responseBodyStream.Position = 0;
            await responseBodyStream.CopyToAsync(originalBody);
        }
        finally
        {
            context.Response.Body = originalBody;
        }
    }

    private bool ShouldApplyCacheControl(HttpRequest request)
    {
        // 检查请求方法
        if (request.Method != "GET" && request.Method != "HEAD")
        {
            return false;
        }
        
        var path = request.Path.Value;
        
        // 检查排除路径
        if (_options.ExcludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        // 检查不可缓存路径
        if (_options.NonCacheablePaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            // 需要添加禁止缓存的头部
            return true;
        }
        
        return true;
    }

    private bool IsConditionalRequest(HttpContext context)
    {
        var request = context.Request;
        
        // 检查If-None-Match(ETag)
        if (_options.EnableETag && request.Headers.ContainsKey("If-None-Match"))
        {
            return true;
        }
        
        // 检查If-Modified-Since(Last-Modified)
        if (_options.EnableLastModified && request.Headers.ContainsKey("If-Modified-Since"))
        {
            return true;
        }
        
        return false;
    }

    private async Task<bool> ValidateConditionalsAsync(HttpContext context)
    {
        var request = context.Request;
        
        // 获取资源的实际路径
        var resourcePath = GetResourcePath(context);
        if (string.IsNullOrEmpty(resourcePath) || !File.Exists(resourcePath))
        {
            return false;
        }
        
        var fileInfo = new FileInfo(resourcePath);
        
        // 验证If-Modified-Since
        if (request.Headers.TryGetValue("If-Modified-Since", out var ifModifiedSince))
        {
            if (DateTime.TryParse(ifModifiedSince, out var modifiedSinceDate))
            {
                // 使用1秒的精度比较(HTTP日期标头不包含毫秒)
                if (fileInfo.LastWriteTimeUtc.AddSeconds(-fileInfo.LastWriteTimeUtc.Second % 1) <= 
                    modifiedSinceDate.ToUniversalTime().AddSeconds(-modifiedSinceDate.Second % 1))
                {
                    return true;
                }
            }
        }
        
        // 验证If-None-Match
        if (request.Headers.TryGetValue("If-None-Match", out var ifNoneMatch))
        {
            var clientETag = ifNoneMatch.ToString().Trim('"');
            
            // 读取文件内容计算ETag
            using var fileStream = new FileStream(resourcePath, FileMode.Open, FileAccess.Read);
            var fileBytes = new byte[fileStream.Length];
            await fileStream.ReadAsync(fileBytes, 0, fileBytes.Length);
            
            // 使用默认生成器生成ETag
            var serverETag = GenerateETagDefault(fileBytes);
            
            if (clientETag == serverETag)
            {
                return true;
            }
        }
        
        return false;
    }

    private string GetResourcePath(HttpContext context)
    {
        // 尝试获取物理文件路径
        var path = context.Request.Path.Value.TrimStart('/');
        var contentRoot = context.RequestServices.GetRequiredService<IWebHostEnvironment>().ContentRootPath;
        var webRoot = context.RequestServices.GetRequiredService<IWebHostEnvironment>().WebRootPath;
        
        // 首先尝试作为静态文件在wwwroot中查找
        var webRootPath = Path.Combine(webRoot, path.Replace('/', Path.DirectorySeparatorChar));
        if (File.Exists(webRootPath))
        {
            return webRootPath;
        }
        
        // 然后尝试在内容根目录中查找
        var contentRootPath = Path.Combine(contentRoot, path.Replace('/', Path.DirectorySeparatorChar));
        if (File.Exists(contentRootPath))
        {
            return contentRootPath;
        }
        
        return null;
    }

    private CacheDecision GetCacheDecision(HttpContext context)
    {
        // 如果提供了委托,优先使用它
        if (_options.CacheDecisionDelegate != null)
        {
            var decision = _options.CacheDecisionDelegate(context);
            if (decision != null)
            {
                return decision;
            }
        }
        
        // 获取缓存配置文件
        var profile = GetCacheProfile(context);
        
        // 判断是否应该缓存
        var shouldCache = ShouldCacheResponse(context, profile);
        
        return new CacheDecision
        {
            ShouldCache = shouldCache,
            Duration = profile?.Duration,
            Location = profile?.Location ?? _options.DefaultCacheLocation,
            NoStore = profile?.NoStore ?? false,
            MustRevalidate = profile?.MustRevalidate ?? false,
            VaryByHeaders = profile?.VaryByHeaders ?? _options.DefaultVaryHeaders
        };
    }

    private CacheProfile GetCacheProfile(HttpContext context)
    {
        // 首先检查路径匹配的配置文件
        var path = context.Request.Path.Value;
        foreach (var pathProfile in _options.PathProfiles)
        {
            if (path.StartsWith(pathProfile.Key, StringComparison.OrdinalIgnoreCase))
            {
                return pathProfile.Value;
            }
        }
        
        // 然后检查控制器上的[ResponseCache]特性
        if (context.Items.TryGetValue("ResponseCacheAttribute", out var attr) && 
            attr is Microsoft.AspNetCore.Mvc.ResponseCacheAttribute cacheAttr)
        {
            var profileName = cacheAttr.CacheProfileName;
            
            if (!string.IsNullOrEmpty(profileName) && _options.CacheProfiles.TryGetValue(profileName, out var namedProfile))
            {
                return namedProfile;
            }
            
            // 从特性创建配置文件
            return new CacheProfile
            {
                Duration = cacheAttr.Duration > 0 ? TimeSpan.FromSeconds(cacheAttr.Duration) : (TimeSpan?)null,
                Location = cacheAttr.Location switch
                {
                    Microsoft.AspNetCore.Mvc.ResponseCacheLocation.Any => CacheLocation.Any,
                    Microsoft.AspNetCore.Mvc.ResponseCacheLocation.Client => CacheLocation.Private,
                    Microsoft.AspNetCore.Mvc.ResponseCacheLocation.None => CacheLocation.NoCache,
                    _ => CacheLocation.Private
                },
                NoStore = cacheAttr.NoStore,
                VaryByHeaders = cacheAttr.VaryByHeader?.Split(',').Select(h => h.Trim()).ToList() ?? new List<string>()
            };
        }
        
        // 使用默认配置
        return new CacheProfile
        {
            Duration = TimeSpan.FromMinutes(5),
            Location = _options.DefaultCacheLocation,
            VaryByHeaders = _options.DefaultVaryHeaders
        };
    }

    private bool ShouldCacheResponse(HttpContext context, CacheProfile profile)
    {
        // 检查状态码(只缓存成功响应)
        if (context.Response.StatusCode < 200 || context.Response.StatusCode >= 300)
        {
            return false;
        }
        
        // 如果配置为NoStore,不缓存
        if (profile?.NoStore == true)
        {
            return false;
        }
        
        // 如果已经设置了Cache-Control: no-store,不缓存
        if (context.Response.Headers.TryGetValue("Cache-Control", out var cacheControl) &&
            cacheControl.ToString().Contains("no-store"))
        {
            return false;
        }
        
        // 检查内容类型
        var contentType = context.Response.ContentType;
        if (string.IsNullOrEmpty(contentType))
        {
            return false;
        }
        
        // 提取主MIME类型
        var mimeType = contentType.Split(';')[0].Trim();
        if (!_options.CacheableContentTypes.Any(t => mimeType.StartsWith(t, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        // 如果配置为不缓存已认证请求,且用户已认证,不缓存
        if (_options.DisableCacheForAuthenticatedRequests && 
            context.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }
        
        return true;
    }

    private async Task ApplyCacheHeadersAsync(HttpContext context, CacheDecision decision, MemoryStream responseBodyStream)
    {
        // 处理Cache-Control头
        if (_options.EnableCacheControl)
        {
            var cacheControlBuilder = new StringBuilder();
            
            // 缓存位置
            switch (decision.Location)
            {
                case CacheLocation.Public:
                    cacheControlBuilder.Append("public");
                    break;
                case CacheLocation.Private:
                    cacheControlBuilder.Append("private");
                    break;
                case CacheLocation.NoCache:
                    cacheControlBuilder.Append("no-cache");
                    break;
            }
            
            // 缓存时间
            if (decision.Duration.HasValue)
            {
                if (cacheControlBuilder.Length > 0)
                    cacheControlBuilder.Append(", ");
                cacheControlBuilder.Append($"max-age={Math.Max(1, (int)decision.Duration.Value.TotalSeconds)}");
            }
            
            // 其他指令
            if (decision.MustRevalidate)
            {
                if (cacheControlBuilder.Length > 0)
                    cacheControlBuilder.Append(", ");
                cacheControlBuilder.Append("must-revalidate");
            }
            
            if (decision.NoStore)
            {
                if (cacheControlBuilder.Length > 0)
                    cacheControlBuilder.Append(", ");
                cacheControlBuilder.Append("no-store");
            }
            
            context.Response.Headers["Cache-Control"] = cacheControlBuilder.ToString();
        }
        
        // 处理Vary头
        if (_options.EnableVaryHeader && decision.VaryByHeaders?.Count > 0)
        {
            context.Response.Headers["Vary"] = string.Join(", ", decision.VaryByHeaders);
        }
        
        // 处理ETag头
        if (_options.EnableETag)
        {
            responseBodyStream.Position = 0;
            var responseBytes = new byte[responseBodyStream.Length];
            await responseBodyStream.ReadAsync(responseBytes, 0, responseBytes.Length);
            
            // 根据配置文件选择ETag生成器
            var profile = GetCacheProfile(context);
            var etagGenerator = profile?.ETagGenerator ?? "default";
            
            if (_etagGenerators.TryGetValue(etagGenerator, out var generator))
            {
                var etag = generator(responseBytes);
                context.Response.Headers["ETag"] = $"\"{etag}\"";
            }
            else
            {
                // 使用默认生成器
                var etag = GenerateETagDefault(responseBytes);
                context.Response.Headers["ETag"] = $"\"{etag}\"";
            }
            
            // 重置流位置以便后续读取
            responseBodyStream.Position = 0;
        }
        
        // 处理Last-Modified头
        if (_options.EnableLastModified)
        {
            // 如果是静态文件,使用文件最后修改时间
            var resourcePath = GetResourcePath(context);
            if (!string.IsNullOrEmpty(resourcePath) && File.Exists(resourcePath))
            {
                var fileInfo = new FileInfo(resourcePath);
                context.Response.Headers["Last-Modified"] = fileInfo.LastWriteTimeUtc.ToString("R");
            }
            else
            {
                // 使用当前时间
                context.Response.Headers["Last-Modified"] = DateTime.UtcNow.ToString("R");
            }
        }
        
        // 处理Expires头(如果设置了Duration)
        if (decision.Duration.HasValue)
        {
            context.Response.Headers["Expires"] = DateTime.UtcNow.Add(decision.Duration.Value).ToString("R");
        }
    }

    private void AddNoCacheHeaders(HttpResponse response)
    {
        // 添加禁止缓存的头部
        response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
        response.Headers["Pragma"] = "no-cache";
        response.Headers["Expires"] = "0";
    }

    #region ETag生成器
    
    private string GenerateETagDefault(byte[] data)
    {
        if (data == null || data.Length == 0)
            return "0";
            
        // 简单的哈希函数,使用前128字节和长度
        int hash = 17;
        int maxBytes = Math.Min(data.Length, 128);
        
        for (int i = 0; i < maxBytes; i++)
        {
            hash = hash * 31 + data[i];
        }
        
        hash = hash * 31 + data.Length;
        
        return hash.ToString("x8");
    }
    
    private string GenerateETagMd5(byte[] data)
    {
        using var md5 = MD5.Create();
        var hash = md5.ComputeHash(data);
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }
    
    private string GenerateETagSha1(byte[] data)
    {
        using var sha1 = SHA1.Create();
        var hash = sha1.ComputeHash(data);
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }
    
    private string GenerateETagSha256(byte[] data)
    {
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(data);
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }
    
    private string GenerateETagMurmur(byte[] data)
    {
        // 简化版MurmurHash3实现
        const uint seed = 0xc58f1a7b;
        const uint c1 = 0xcc9e2d51;
        const uint c2 = 0x1b873593;
        
        uint h1 = seed;
        uint length = (uint)data.Length;
        uint blocks = length / 4;
        
        for (uint i = 0; i < blocks; i++)
        {
            uint k1 = BitConverter.ToUInt32(data, (int)(i * 4));
            
            k1 *= c1;
            k1 = RotateLeft(k1, 15);
            k1 *= c2;
            
            h1 ^= k1;
            h1 = RotateLeft(h1, 13);
            h1 = h1 * 5 + 0xe6546b64;
        }
        
        uint tailIndex = blocks * 4;
        uint k2 = 0;
        
        switch (length & 3)
        {
            case 3:
                k2 ^= (uint)data[tailIndex + 2] << 16;
                goto case 2;
            case 2:
                k2 ^= (uint)data[tailIndex + 1] << 8;
                goto case 1;
            case 1:
                k2 ^= data[tailIndex];
                k2 *= c1;
                k2 = RotateLeft(k2, 15);
                k2 *= c2;
                h1 ^= k2;
                break;
        }
        
        h1 ^= length;
        h1 = FinalMix(h1);
        
        return h1.ToString("x8");
    }
    
    private uint RotateLeft(uint x, byte r)
    {
        return (x << r) | (x >> (32 - r));
    }
    
    private uint FinalMix(uint h)
    {
        h ^= h >> 16;
        h *= 0x85ebca6b;
        h ^= h >> 13;
        h *= 0xc2b2ae35;
        h ^= h >> 16;
        return h;
    }
    
    #endregion
}

public static class HttpCacheMiddlewareExtensions
{
    public static IServiceCollection AddHttpCacheControl(
        this IServiceCollection services,
        Action<HttpCacheOptions> configureOptions = null)
    {
        var options = new HttpCacheOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseHttpCacheControl(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpCacheMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632

注册方式:

// 在Startup.ConfigureServices中
services.AddHttpCacheControl(options => {
    options.EnableETag = true;
    options.EnableLastModified = true;
    options.EnableVaryHeader = true;
    options.EnableCacheControl = true;
    
    // 默认缓存位置
    options.DefaultCacheLocation = CacheLocation.Private;
    
    // 默认Vary头
    options.DefaultVaryHeaders.Add("Accept");
    options.DefaultVaryHeaders.Add("Accept-Encoding");
    
    // 配置缓存配置文件
    options.CacheProfiles["Static"] = new CacheProfile {
        Duration = TimeSpan.FromDays(7),
        Location = CacheLocation.Public,
        VaryByHeaders = new List<string> { "Accept-Encoding" },
        ETagGenerator = "sha1"
    };
    
    options.CacheProfiles["Api"] = new CacheProfile {
        Duration = TimeSpan.FromMinutes(5),
        Location = CacheLocation.Private,
        MustRevalidate = true
    };
    
    // 基于路径的缓存配置
    options.PathProfiles["/css/"] = options.CacheProfiles["Static"];
    options.PathProfiles["/js/"] = options.CacheProfiles["Static"];
    options.PathProfiles["/images/"] = options.CacheProfiles["Static"];
    options.PathProfiles["/api/"] = options.CacheProfiles["Api"];
    
    // 排除的路径
    options.ExcludedPaths.Add("/admin/");
    
    // 不可缓存的路径
    options.NonCacheablePaths.Add("/api/sensitive/");
    
    // 自定义缓存决策
    options.CacheDecisionDelegate = context => {
        // 基于请求和响应做出高级缓存决策
        var path = context.Request.Path.Value;
        
        // 示例:特殊处理会员API
        if (path.StartsWith("/api/members/"))
        {
            return new CacheDecision {
                ShouldCache = true,
                Duration = TimeSpan.FromMinutes(1),
                Location = CacheLocation.Private,
                MustRevalidate = true,
                VaryByHeaders = new List<string> { "Authorization" }
            };
        }
        
        return null; // 使用默认逻辑
    };
});

// 在Startup.Configure中
app.UseHttpCacheControl();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

控制器中使用:

// 使用已定义的缓存配置文件
[ResponseCache(CacheProfileName = "Api")]
[HttpGet("products")]
public IActionResult GetProducts()
{
    // ...
}

// 直接在控制器/操作上指定缓存参数
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Client, VaryByHeader = "Accept-Language")]
[HttpGet("categories")]
public IActionResult GetCategories()
{
    // ...
}

// 禁用缓存
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
[HttpGet("user-profile")]
public IActionResult GetUserProfile()
{
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

应用场景: 优化Web应用和API性能,减少服务器负载和网络流量,缩短页面加载时间。适用于内容网站、电子商务平台、API服务和静态资产。

市场集成: 与CDN服务(如Cloudflare、Akamai)集成,优化边缘缓存;与Redis或其他分布式缓存集成,实现多服务器缓存共享。

# 19. 强制HTTPS和HSTS中间件

用途: 确保所有请求使用HTTPS,并实现HSTS(HTTP严格传输安全),提升网站安全性。

实现:

public class HttpsOptions
{
    public bool EnableForceHttps { get; set; } = true;
    public bool EnableHsts { get; set; } = true;
    public int HttpsPort { get; set; } = 443;
    public TimeSpan HstsMaxAge { get; set; } = TimeSpan.FromDays(30);
    public bool HstsIncludeSubDomains { get; set; } = true;
    public bool HstsPreload { get; set; } = false;
    public List<string> ExcludedHosts { get; set; } = new List<string> { "localhost", "127.0.0.1" };
    public List<string> ExcludedPaths { get; set; } = new List<string>();
    public bool RequireHttpsPermanent { get; set; } = true;
    public bool UseStrictPolicyHeaders { get; set; } = true;
    public bool DisallowHttp1 { get; set; } = false;
    public bool LogSslErrors { get; set; } = true;
    public bool AllowWeakCiphers { get; set; } = false;
    public bool UseSecureCookies { get; set; } = true;
}

public class HttpsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly HttpsOptions _options;
    private readonly ILogger<HttpsMiddleware> _logger;

    public HttpsMiddleware(
        RequestDelegate next,
        HttpsOptions options,
        ILogger<HttpsMiddleware> logger)
    {
        _next = next;
        _options = options;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否应该强制HTTPS
        if (_options.EnableForceHttps && !context.Request.IsHttps)
        {
            if (ShouldRedirectToHttps(context))
            {
                RedirectToHttps(context);
                return;
            }
        }
        
        // 如果已经是HTTPS请求,应用HSTS策略
        if (context.Request.IsHttps && _options.EnableHsts)
        {
            if (ShouldApplyHsts(context))
            {
                ApplyHstsHeader(context);
            }
        }
        
        // 添加安全相关头部
        if (_options.UseStrictPolicyHeaders && context.Request.IsHttps)
        {
            AddSecurityHeaders(context);
        }
        
        // 确保安全Cookie策略
        if (_options.UseSecureCookies && context.Request.IsHttps)
        {
            EnsureSecureCookies(context);
        }
        
        await _next(context);
    }

    private bool ShouldRedirectToHttps(HttpContext context)
    {
        var host = context.Request.Host.Host;
        
        // 检查排除的主机
        if (_options.ExcludedHosts.Contains(host))
        {
            return false;
        }
        
        // 检查排除的路径
        var path = context.Request.Path.Value;
        if (_options.ExcludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }
        
        return true;
    }

    private void RedirectToHttps(HttpContext context)
    {
        var host = context.Request.Host;
        var newHost = new HostString(host.Host, _options.HttpsPort);
        
        var redirectUrl = string.Concat(
            "https://",
            newHost.ToUriComponent(),
            context.Request.PathBase.ToUriComponent(),
            context.Request.Path.ToUriComponent(),
            context.Request.QueryString.ToUriComponent());
            
        // 使用301(永久)或302(临时)重定向
        context.Response.StatusCode = _options.RequireHttpsPermanent ? 
            StatusCodes.Status301MovedPermanently : 
            StatusCodes.Status302Found;
            
        context.Response.Headers["Location"] = redirectUrl;
    }

    private bool ShouldApplyHsts(HttpContext context)
    {
        var host = context.Request.Host.Host;
        
        // HSTS不应该应用于IP地址和排除的主机
        if (IsIpAddress(host) || _options.ExcludedHosts.Contains(host))
        {
            return false;
        }
        
        return true;
    }

    private void ApplyHstsHeader(HttpContext context)
    {
        var maxAge = (int)_options.HstsMaxAge.TotalSeconds;
        var hstsValue = $"max-age={maxAge}";
        
        if (_options.HstsIncludeSubDomains)
        {
            hstsValue += "; includeSubDomains";
        }
        
        if (_options.HstsPreload)
        {
            hstsValue += "; preload";
        }
        
        context.Response.Headers["Strict-Transport-Security"] = hstsValue;
    }

    private void AddSecurityHeaders(HttpContext context)
    {
        // 内容安全策略
        context.Response.Headers["Content-Security-Policy"] = 
            "default-src 'self'; upgrade-insecure-requests;";
            
        // 防止MIME类型嗅探
        context.Response.Headers["X-Content-Type-Options"] = "nosniff";
        
        // 防止点击劫持
        context.Response.Headers["X-Frame-Options"] = "DENY";
        
        // 启用XSS保护
        context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
        
        // 引荐来源策略
        context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
        
        // 特性策略
        context.Response.Headers["Feature-Policy"] = 
            "camera 'none'; microphone 'none'; geolocation 'none';";
            
        // HTTP/2和HTTP/3检测
        if (_options.DisallowHttp1 && HttpProtocolChecker.IsHttp2OrHigher(context))
        {
            context.Response.Headers["X-Protocol-Enforced"] = "h2+";
        }
    }

    private void EnsureSecureCookies(HttpContext context)
    {
        // 在响应发送前确保所有cookie都有Secure标志
        context.Response.OnStarting(() => {
            var cookies = context.Response.Headers["Set-Cookie"];
            if (!string.IsNullOrEmpty(cookies))
            {
                var cookiesList = cookies.ToArray();
                for (var i = 0; i < cookiesList.Length; i++)
                {
                    var cookie = cookiesList[i];
                    if (!cookie.Contains("; secure", StringComparison.OrdinalIgnoreCase) &&
                        !cookie.EndsWith("; secure", StringComparison.OrdinalIgnoreCase))
                    {
                        cookiesList[i] = cookie + "; secure; samesite=strict";
                    }
                }
                
                context.Response.Headers["Set-Cookie"] = cookiesList;
            }
            
            return Task.CompletedTask;
        });
    }

    private bool IsIpAddress(string host)
    {
        // 检查是否是IPv4地址
        if (IPAddress.TryParse(host, out _))
        {
            return true;
        }
        
        // 检查是否是以[开头的IPv6地址
        if (host.StartsWith("[") && host.Contains("]"))
        {
            var ipv6 = host.Substring(1, host.IndexOf("]") - 1);
            return IPAddress.TryParse(ipv6, out _);
        }
        
        return false;
    }
}

public static class HttpProtocolChecker
{
    public static bool IsHttp2OrHigher(HttpContext context)
    {
        // 注意:此检查在Kestrel上适用,但在IIS或其他服务器可能需要不同的逻辑
        var connectionFeature = context.Features.Get<IHttpConnectionFeature>();
        if (connectionFeature != null)
        {
            var protocol = connectionFeature.Protocol;
            return protocol != null && (protocol.Equals("HTTP/2", StringComparison.OrdinalIgnoreCase) ||
                                      protocol.Equals("HTTP/3", StringComparison.OrdinalIgnoreCase));
        }
        
        return false;
    }
}

public static class HttpsMiddlewareExtensions
{
    public static IServiceCollection AddHttpsEnforcement(
        this IServiceCollection services,
        Action<HttpsOptions> configureOptions = null)
    {
        var options = new HttpsOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseHttpsEnforcement(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpsMiddleware>();
    }
    
    // 用于集成ASP.NET Core内置HTTPS重定向中间件的扩展方法
    public static IApplicationBuilder UseHttpsEnforcementWithCoreMiddleware(this IApplicationBuilder builder, HttpsOptions options)
    {
        // 添加ASP.NET Core内置的HTTPS重定向中间件
        if (options.EnableForceHttps)
        {
            builder.UseHttpsRedirection();
        }
        
        // 添加ASP.NET Core内置的HSTS中间件
        if (options.EnableHsts)
        {
            builder.UseHsts();
        }
        
        // 添加自定义中间件用于高级安全头部
        return builder.UseMiddleware<HttpsMiddleware>(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269

注册方式:

// 在Startup.ConfigureServices中
services.AddHttpsEnforcement(options => {
    options.EnableForceHttps = true;
    options.EnableHsts = true;
    options.HttpsPort = 443;
    
    // HSTS配置
    options.HstsMaxAge = TimeSpan.FromDays(365); // 1年
    options.HstsIncludeSubDomains = true;
    options.HstsPreload = true; // 允许加入浏览器预加载列表
    
    // 排除本地开发环境
    options.ExcludedHosts.Add("localhost");
    options.ExcludedHosts.Add("127.0.0.1");
    
    // 安全头部和Cookie策略
    options.UseStrictPolicyHeaders = true;
    options.UseSecureCookies = true;
});

// 在Startup.Configure中
// 注意:应在其他中间件之前添加,以确保所有请求都通过HTTPS
app.UseHttpsEnforcement();

// 或者,与ASP.NET Core内置中间件集成
app.UseHttpsEnforcementWithCoreMiddleware(app.ApplicationServices.GetRequiredService<HttpsOptions>());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

应用场景: 保护网站流量,防止中间人攻击,提高安全性。适用于处理敏感信息的网站,如电子商务、银行、医疗和任何需要保护用户数据的应用。

市场集成: 与SSL证书管理服务集成;与安全扫描工具集成,自动检测安全配置问题;与WAF(Web应用防火墙)集成,增强安全防护。

# 20. 智能返回文件中间件

用途: 智能处理文件下载请求,支持断点续传、范围请求、文件压缩和格式转换。

实现:

public class SmartFileOptions
{
    public bool EnableRangeProcessing { get; set; } = true;
    public bool EnableContentDisposition { get; set; } = true;
    public bool EnableCompression { get; set; } = true;
    public bool EnableETags { get; set; } = true;
    public bool EnableDirectoryBrowsing { get; set; } = false;
    public int BufferSize { get; set; } = 64 * 1024; // 64KB
    public long MaxRangeLength { get; set; } = 10 * 1024 * 1024; // 10MB
    public string FilesPath { get; set; }
    public TimeSpan CacheDuration { get; set; } = TimeSpan.FromDays(1);
    public Dictionary<string, string> ContentTypeMap { get; set; } = new Dictionary<string, string>();
    public List<string> CompressibleTypes { get; set; } = new List<string>();
    public List<string> AllowedExtensions { get; set; } = new List<string>();
    public List<string> DeniedExtensions { get; set; } = new List<string>();
    public bool AllowCrossOrigin { get; set; } = false;
    public List<string> DirectoryIndexFiles { get; set; } = new List<string> { "index.html", "index.htm", "default.html" };
    public List<string> AllowedPaths { get; set; } = new List<string>();
    public bool ValidateFilenames { get; set; } = true;
    public Func<string, string, bool> AuthorizationHandler { get; set; }
    public bool EnableImageProcessing { get; set; } = false;
    public bool EnableOnTheFlyConversion { get; set; } = false;
    public bool TrackDownloads { get; set; } = false;
}

public class FileRangeResult
{
    public long Start { get; set; }
    public long End { get; set; }
    public long Length { get; set; }
    public long TotalLength { get; set; }
    public bool IsPartial { get; set; }
}

public class SmartFileMiddleware
{
    private readonly RequestDelegate _next;
    private readonly SmartFileOptions _options;
    private readonly ILogger<SmartFileMiddleware> _logger;
    private readonly IWebHostEnvironment _env;

    // 常见的MIME类型映射
    private static readonly Dictionary<string, string> DefaultContentTypes = new Dictionary<string, string>
    {
        [".txt"] = "text/plain",
        [".html"] = "text/html",
        [".htm"] = "text/html",
        [".css"] = "text/css",
        [".js"] = "application/javascript",
        [".json"] = "application/json",
        [".jpg"] = "image/jpeg",
        [".jpeg"] = "image/jpeg",
        [".png"] = "image/png",
        [".gif"] = "image/gif",
        [".svg"] = "image/svg+xml",
        [".pdf"] = "application/pdf",
        [".doc"] = "application/msword",
        [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        [".xls"] = "application/vnd.ms-excel",
        [".xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        [".zip"] = "application/zip",
        [".mp3"] = "audio/mpeg",
        [".mp4"] = "video/mp4",
        [".webm"] = "video/webm",
        [".ogg"] = "audio/ogg",
        [".wav"] = "audio/wav",
        [".xml"] = "application/xml",
        [".csv"] = "text/csv"
    };

    // 可压缩的内容类型
    private static readonly HashSet<string> DefaultCompressibleTypes = new HashSet<string>
    {
        "text/plain",
        "text/html",
        "text/css",
        "text/javascript",
        "application/javascript",
        "application/json",
        "application/xml",
        "text/xml",
        "text/csv"
    };

    public SmartFileMiddleware(
        RequestDelegate next,
        SmartFileOptions options,
        ILogger<SmartFileMiddleware> logger,
        IWebHostEnvironment env)
    {
        _next = next;
        _options = options;
        _logger = logger;
        _env = env;
        
        // 合并默认内容类型与用户提供的映射
        foreach (var contentType in DefaultContentTypes)
        {
            if (!_options.ContentTypeMap.ContainsKey(contentType.Key))
            {
                _options.ContentTypeMap[contentType.Key] = contentType.Value;
            }
        }
        
        // 如果未配置压缩类型,使用默认的
        if (_options.CompressibleTypes.Count == 0)
        {
            _options.CompressibleTypes.AddRange(DefaultCompressibleTypes);
        }
        
        // 如果未配置文件路径,使用web根目录
        if (string.IsNullOrEmpty(_options.FilesPath))
        {
            _options.FilesPath = _env.WebRootPath;
        }
        
        // 确保文件目录存在
        if (!Directory.Exists(_options.FilesPath))
        {
            Directory.CreateDirectory(_options.FilesPath);
        }
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 检查是否是文件请求
        if (!IsFileRequest(context.Request))
        {
            await _next(context);
            return;
        }
        
        // 解析文件路径
        var filePath = ResolveFilePath(context.Request);
        if (string.IsNullOrEmpty(filePath))
        {
            _logger.LogWarning("Invalid file path requested");
            context.Response.StatusCode = 404;
            return;
        }
        
        // 检查文件是否存在
        if (!File.Exists(filePath))
        {
            // 检查是否是目录
            if (Directory.Exists(filePath))
            {
                if (_options.EnableDirectoryBrowsing)
                {
                    // 查找目录索引文件
                    var indexFile = FindDirectoryIndex(filePath);
                    if (!string.IsNullOrEmpty(indexFile))
                    {
                        filePath = indexFile;
                    }
                    else
                    {
                        // 显示目录内容
                        await ServeDirctoryListingAsync(context, filePath);
                        return;
                    }
                }
                else
                {
                    _logger.LogWarning("Directory browsing is disabled: {Path}", filePath);
                    context.Response.StatusCode = 403; // Forbidden
                    return;
                }
            }
            else
            {
                _logger.LogWarning("File not found: {Path}", filePath);
                context.Response.StatusCode = 404; // Not Found
                return;
            }
        }
        
        // 检查文件授权
        if (!AuthorizeFile(context, filePath))
        {
            _logger.LogWarning("Access denied to file: {Path}", filePath);
            context.Response.StatusCode = 403; // Forbidden
            return;
        }
        
        // 获取文件信息
        var fileInfo = new FileInfo(filePath);
        var fileExtension = fileInfo.Extension.ToLowerInvariant();
        
        // 检查是否允许的扩展名
        if (!IsAllowedExtension(fileExtension))
        {
            _logger.LogWarning("File extension not allowed: {Extension}", fileExtension);
            context.Response.StatusCode = 403; // Forbidden
            return;
        }
        
        // 检查条件请求
        if (IsConditionalRequest(context.Request) && !IsModified(context, fileInfo))
        {
            context.Response.StatusCode = 304; // Not Modified
            return;
        }
        
        // 处理范围请求
        FileRangeResult range = null;
        if (_options.EnableRangeProcessing && IsRangeRequest(context.Request))
        {
            range = ParseRange(context.Request, fileInfo.Length);
            if (range == null || !range.IsPartial)
            {
                // 无效的范围,返回整个文件
                range = new FileRangeResult
                {
                    Start = 0,
                    End = fileInfo.Length - 1,
                    Length = fileInfo.Length,
                    TotalLength = fileInfo.Length,
                    IsPartial = false
                };
            }
            else if (range.Start >= fileInfo.Length || range.End >= fileInfo.Length)
            {
                // 范围超出文件长度
                context.Response.StatusCode = 416; // Range Not Satisfiable
                context.Response.Headers["Content-Range"] = $"bytes */{fileInfo.Length}";
                return;
            }
        }
        else
        {
            // 不是范围请求,返回整个文件
            range = new FileRangeResult
            {
                Start = 0,
                End = fileInfo.Length - 1,
                Length = fileInfo.Length,
                TotalLength = fileInfo.Length,
                IsPartial = false
            };
        }
        
        // 获取内容类型
        var contentType = GetContentType(fileExtension);
        context.Response.ContentType = contentType;
        
        // 设置内容处置头(下载或内联)
        if (_options.EnableContentDisposition)
        {
            SetContentDisposition(context, fileInfo.Name);
        }
        
        // 设置缓存头
        SetCacheHeaders(context, fileInfo);
        
        // 设置ETag
        if (_options.EnableETags)
        {
            SetETagHeader(context, fileInfo);
        }
        
        // 设置范围响应头
        if (range.IsPartial)
        {
            context.Response.StatusCode = 206; // Partial Content
            context.Response.Headers["Content-Range"] = $"bytes {range.Start}-{range.End}/{range.TotalLength}";
            context.Response.ContentLength = range.Length;
        }
        else
        {
            context.Response.ContentLength = fileInfo.Length;
        }
        
        // 设置跨域头
        if (_options.AllowCrossOrigin)
        {
            context.Response.Headers["Access-Control-Allow-Origin"] = "*";
            context.Response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS";
            context.Response.Headers["Access-Control-Allow-Headers"] = "Range, If-None-Match, If-Modified-Since";
            context.Response.Headers["Access-Control-Expose-Headers"] = "Content-Length, Content-Range, Accept-Ranges";
        }
        
        // 只有GET请求才发送文件内容,HEAD请求只发送头部
        if (HttpMethods.IsGet(context.Request.Method))
        {
            // 检查是否可以压缩
            var shouldCompress = _options.EnableCompression && 
                                DefaultCompressibleTypes.Contains(contentType) &&
                                context.Request.Headers.TryGetValue("Accept-Encoding", out var encodings) &&
                                encodings.ToString().Contains("gzip");
                                
            // 发送文件
            await SendFileAsync(context, filePath, range, shouldCompress);
            
            // 跟踪下载(如果启用)
            if (_options.TrackDownloads)
            {
                TrackDownload(context, filePath, range);
            }
        }
    }

    private bool IsFileRequest(HttpRequest request)
    {
        // 只处理GET和HEAD请求
        if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
        {
            return false;
        }
        
        // 检查路径是否在允许列表中(如果指定)
        if (_options.AllowedPaths.Count > 0)
        {
            var path = request.Path.Value;
            if (!_options.AllowedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
            {
                return false;
            }
        }
        
        return true;
    }

    private string ResolveFilePath(HttpRequest request)
    {
        var requestPath = request.Path.Value.TrimStart('/');
        
        // 验证文件名,防止目录遍历攻击
        if (_options.ValidateFilenames && !IsValidFilename(requestPath))
        {
            _logger.LogWarning("Invalid filename: {Path}", requestPath);
            return null;
        }
        
        // 解析完整文件路径
        var filePath = Path.Combine(_options.FilesPath, requestPath.Replace('/', Path.DirectorySeparatorChar));
        
        // 规范化路径,确保它在文件根目录下
        filePath = Path.GetFullPath(filePath);
        var rootPath = Path.GetFullPath(_options.FilesPath);
        
        if (!filePath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
        {
            _logger.LogWarning("Path traversal attempt: {Path}", filePath);
            return null;
        }
        
        return filePath;
    }

    private bool IsValidFilename(string filename)
    {
        // 检查无效字符和模式
        return !filename.Contains("..", StringComparison.OrdinalIgnoreCase) &&
               !Path.GetInvalidPathChars().Any(c => filename.Contains(c));
    }

    private bool IsAllowedExtension(string extension)
    {
        // 如果指定了允许的扩展名,检查是否在列表中
        if (_options.AllowedExtensions.Count > 0)
        {
            return _options.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
        }
        
        // 如果指定了拒绝的扩展名,检查是否不在列表中
        if (_options.DeniedExtensions.Count > 0)
        {
            return !_options.DeniedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
        }
        
        // 默认允许所有扩展名
        return true;
    }

    private bool AuthorizeFile(HttpContext context, string filePath)
    {
        // 如果提供了授权处理程序,使用它
        if (_options.AuthorizationHandler != null)
        {
            var relativePath = GetRelativePath(filePath);
            return _options.AuthorizationHandler(relativePath, context.User?.Identity?.Name);
        }
        
        // 默认允许访问
        return true;
    }

    private string GetRelativePath(string filePath)
    {
        var rootPath = Path.GetFullPath(_options.FilesPath);
        var fullPath = Path.GetFullPath(filePath);
        
        if (fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
        {
            var relativePath = fullPath.Substring(rootPath.Length).TrimStart('\\', '/');
            return relativePath.Replace('\\', '/');
        }
        
        return string.Empty;
    }

    private string FindDirectoryIndex(string directoryPath)
    {
        foreach (var indexFile in _options.DirectoryIndexFiles)
        {
            var filePath = Path.Combine(directoryPath, indexFile);
            if (File.Exists(filePath))
            {
                return filePath;
            }
        }
        
        return null;
    }

    private async Task ServeDirctoryListingAsync(HttpContext context, string directoryPath)
    {
        var directoryInfo = new DirectoryInfo(directoryPath);
        var rootPath = Path.GetFullPath(_options.FilesPath);
        var relativePath = GetRelativePath(directoryPath);
        
        // 构建HTML目录列表
        var sb = new StringBuilder();
        sb.AppendLine("<!DOCTYPE html>");
        sb.AppendLine("<html>");
        sb.AppendLine("<head>");
        sb.AppendLine($"<title>Directory Listing: /{relativePath}</title>");
        sb.AppendLine("<style>");
        sb.AppendLine("body { font-family: Arial, sans-serif; margin: 20px; }");
        sb.AppendLine("h1 { color: #333; }");
        sb.AppendLine("table { border-collapse: collapse; width: 100%; }");
        sb.AppendLine("th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }");
        sb.AppendLine("tr:nth-child(even) { background-color: #f2f2f2; }");
        sb.AppendLine("a { text-decoration: none; color: #0366d6; }");
        sb.AppendLine("a:hover { text-decoration: underline; }");
        sb.AppendLine(".icon { margin-right: 5px; }");
        sb.AppendLine("</style>");
        sb.AppendLine("</head>");
        sb.AppendLine("<body>");
        sb.AppendLine($"<h1>Directory Listing: /{relativePath}</h1>");
        
        // 添加上级目录链接
        if (!string.IsNullOrEmpty(relativePath))
        {
            var parentPath = Path.GetDirectoryName(relativePath.Replace('/', Path.DirectorySeparatorChar))?.Replace('\\', '/');
            sb.AppendLine($"<p><a href=\"/{parentPath ?? ""}\">&#8617; Parent Directory</a></p>");
        }
        
        sb.AppendLine("<table>");
        sb.AppendLine("<tr><th>Name</th><th>Size</th><th>Last Modified</th></tr>");
        
        // 添加目录
        foreach (var directory in directoryInfo.GetDirectories().OrderBy(d => d.Name))
        {
            var dirRelativePath = Path.Combine(relativePath, directory.Name).Replace('\\', '/');
            sb.AppendLine("<tr>");
            sb.AppendLine($"<td><a href=\"/{dirRelativePath}\">📁 {directory.Name}/</a></td>");
            sb.AppendLine("<td>-</td>");
            sb.AppendLine($"<td>{directory.LastWriteTime}</td>");
            sb.AppendLine("</tr>");
        }
        
        // 添加文件
        foreach (var file in directoryInfo.GetFiles().OrderBy(f => f.Name))
        {
            var fileRelativePath = Path.Combine(relativePath, file.Name).Replace('\\', '/');
            sb.AppendLine("<tr>");
            sb.AppendLine($"<td><a href=\"/{fileRelativePath}\">📄 {file.Name}</a></td>");
            sb.AppendLine($"<td>{FormatFileSize(file.Length)}</td>");
            sb.AppendLine($"<td>{file.LastWriteTime}</td>");
            sb.AppendLine("</tr>");
        }
        
        sb.AppendLine("</table>");
        sb.AppendLine("</body>");
        sb.AppendLine("</html>");
        
        context.Response.ContentType = "text/html";
        await context.Response.WriteAsync(sb.ToString());
    }

    private string FormatFileSize(long bytes)
    {
        string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
        int i;
        double dblBytes = bytes;
        
        for (i = 0; i < suffixes.Length && bytes >= 1024; i++, bytes /= 1024)
        {
            dblBytes = bytes / 1024.0;
        }
        
        return $"{dblBytes:0.##} {suffixes[i]}";
    }

    private bool IsConditionalRequest(HttpRequest request)
    {
        return request.Headers.ContainsKey("If-Modified-Since") ||
               request.Headers.ContainsKey("If-None-Match");
    }

    private bool IsModified(HttpContext context, FileInfo fileInfo)
    {
        var request = context.Request;
        
        // 检查ETag
        if (_options.EnableETags && request.Headers.TryGetValue("If-None-Match", out var etag))
        {
            var fileEtag = GenerateETag(fileInfo);
            if (etag.ToString().Trim('"') == fileEtag)
            {
                return false;
            }
        }
        
        // 检查修改时间
        if (request.Headers.TryGetValue("If-Modified-Since", out var ifModifiedSince))
        {
            if (DateTime.TryParse(ifModifiedSince, out var modifiedSinceDate))
            {
                // 使用1秒的精度比较(HTTP日期标头不包含毫秒)
                if (fileInfo.LastWriteTimeUtc.AddSeconds(-fileInfo.LastWriteTimeUtc.Second % 1) <= 
                    modifiedSinceDate.ToUniversalTime().AddSeconds(-modifiedSinceDate.Second % 1))
                {
                    return false;
                }
            }
        }
        
        return true;
    }

    private bool IsRangeRequest(HttpRequest request)
    {
        return request.Headers.ContainsKey("Range");
    }

    private FileRangeResult ParseRange(HttpRequest request, long fileLength)
    {
        if (!request.Headers.TryGetValue("Range", out var rangeHeader))
        {
            return null;
        }
        
        // 解析范围头部,例如: "bytes=0-499"或"bytes=500-"
        var rangeValue = rangeHeader.ToString();
        if (!rangeValue.StartsWith("bytes="))
        {
            return null;
        }
        
        rangeValue = rangeValue.Substring("bytes=".Length);
        var rangeParts = rangeValue.Split('-');
        
        if (rangeParts.Length != 2)
        {
            return null;
        }
        
        // 解析开始位置
        long start, end;
        if (string.IsNullOrEmpty(rangeParts[0]))
        {
            // 如果形式为"-N",表示最后N个字节
            if (!long.TryParse(rangeParts[1], out var lastN) || lastN <= 0)
            {
                return null;
            }
            
            start = Math.Max(0, fileLength - lastN);
            end = fileLength - 1;
        }
        else
        {
            // 标准范围
            if (!long.TryParse(rangeParts[0], out start))
            {
                return null;
            }
            
            if (string.IsNullOrEmpty(rangeParts[1]))
            {
                // 如果形式为"N-",表示从N到文件末尾
                end = fileLength - 1;
            }
            else
            {
                // 如果形式为"N-M",表示从N到M
                if (!long.TryParse(rangeParts[1], out end))
                {
                    return null;
                }
            }
        }
        
        // 验证范围
        if (start < 0 || end < 0 || start > end || start >= fileLength)
        {
            return null;
        }
        
        // 限制范围长度
        if (_options.MaxRangeLength > 0 && (end - start + 1) > _options.MaxRangeLength)
        {
            end = start + _options.MaxRangeLength - 1;
        }
        
        return new FileRangeResult
        {
            Start = start,
            End = Math.Min(end, fileLength - 1),
            Length = Math.Min(end, fileLength - 1) - start + 1,
            TotalLength = fileLength,
            IsPartial = true
        };
    }

    private string GetContentType(string fileExtension)
    {
        if (_options.ContentTypeMap.TryGetValue(fileExtension, out var contentType))
        {
            return contentType;
        }
        
        // 回退到默认映射
        if (DefaultContentTypes.TryGetValue(fileExtension, out contentType))
        {
            return contentType;
        }
        
        // 未知类型
        return "application/octet-stream";
    }

    private void SetContentDisposition(HttpContext context, string fileName)
    {
        // 检查是否应该作为附件下载
        var download = false;
        if (context.Request.Query.TryGetValue("download", out var downloadValue))
        {
            download = string.Equals(downloadValue, "true", StringComparison.OrdinalIgnoreCase) ||
                      string.Equals(downloadValue, "1");
        }
        
        // 或者根据文件类型决定
        var fileExtension = Path.GetExtension(fileName).ToLowerInvariant();
        var contentType = GetContentType(fileExtension);
        
        // 如果不是浏览器可以直接显示的内容类型,则作为附件下载
        if (!contentType.StartsWith("text/") && 
            !contentType.StartsWith("image/") && 
            !contentType.StartsWith("video/") &&
            !contentType.StartsWith("audio/") &&
            contentType != "application/pdf")
        {
            download = true;
        }
        
        // 设置Content-Disposition头部
        var dispositionType = download ? "attachment" : "inline";
        
        // 对文件名进行URL编码
        var encodedFileName = Uri.EscapeDataString(fileName);
        
        // 添加Content-Disposition头部
        context.Response.Headers["Content-Disposition"] = $"{dispositionType}; filename=\"{encodedFileName}\"; filename*=UTF-8''{encodedFileName}";
    }

    private void SetCacheHeaders(HttpContext context, FileInfo fileInfo)
    {
        // 设置Last-Modified头部
        context.Response.Headers["Last-Modified"] = fileInfo.LastWriteTimeUtc.ToString("R");
        
        // 设置Cache-Control和Expires头部
        if (_options.CacheDuration > TimeSpan.Zero)
        {
            var maxAge = (int)_options.CacheDuration.TotalSeconds;
            context.Response.Headers["Cache-Control"] = $"public, max-age={maxAge}";
            context.Response.Headers["Expires"] = DateTime.UtcNow.Add(_options.CacheDuration).ToString("R");
        }
        
        // 添加Accept-Ranges头部,表示支持范围请求
        if (_options.EnableRangeProcessing)
        {
            context.Response.Headers["Accept-Ranges"] = "bytes";
        }
    }

    private void SetETagHeader(HttpContext context, FileInfo fileInfo)
    {
        var etag = GenerateETag(fileInfo);
        context.Response.Headers["ETag"] = $"\"{etag}\"";
    }

    private string GenerateETag(FileInfo fileInfo)
    {
        // 基于文件大小和最后修改时间生成ETag
        var lastModified = fileInfo.LastWriteTimeUtc.Ticks;
        var fileSize = fileInfo.Length;
        var hashValue = $"{fileSize}_{lastModified}";
        
        using var md5 = MD5.Create();
        var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(hashValue));
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }

    private async Task SendFileAsync(HttpContext context, string filePath, FileRangeResult range, bool compress)
    {
        try
        {
            // 对于小文件,使用简单的SendFileAsync
            if (range.Length < 64 * 1024 && !compress)
            {
                await context.Response.SendFileAsync(filePath, range.Start, range.Length);
                return;
            }
            
            // 对于大文件或需要压缩的文件,使用流处理
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            
            if (range.Start > 0)
            {
                fileStream.Seek(range.Start, SeekOrigin.Begin);
            }
            
            if (compress)
            {
                // 设置压缩响应
                context.Response.Headers["Content-Encoding"] = "gzip";
                
                using var gzipStream = new GZipStream(context.Response.Body, CompressionLevel.Fastest, leaveOpen: true);
                await CopyFileRangeAsync(fileStream, gzipStream, range.Length);
            }
            else
            {
                // 直接复制文件范围
                await CopyFileRangeAsync(fileStream, context.Response.Body, range.Length);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error sending file: {Path}", filePath);
            
            // 如果响应尚未开始,设置错误状态码
            if (!context.Response.HasStarted)
            {
                context.Response.StatusCode = 500;
            }
        }
    }

    private async Task CopyFileRangeAsync(Stream source, Stream destination, long length)
    {
        var buffer = new byte[_options.BufferSize];
        var bytesRemaining = length;
        
        while (bytesRemaining > 0)
        {
            var bytesToRead = (int)Math.Min(buffer.Length, bytesRemaining);
            var bytesRead = await source.ReadAsync(buffer, 0, bytesToRead);
            
            if (bytesRead == 0)
                break;
                
            await destination.WriteAsync(buffer, 0, bytesRead);
            bytesRemaining -= bytesRead;
        }
    }

    private void TrackDownload(HttpContext context, string filePath, FileRangeResult range)
    {
        // 简单的下载跟踪记录
        var relativePath = GetRelativePath(filePath);
        var userAgent = context.Request.Headers["User-Agent"].ToString();
        var referer = context.Request.Headers["Referer"].ToString();
        var ipAddress = context.Connection.RemoteIpAddress?.ToString();
        var userId = context.User?.Identity?.IsAuthenticated == true ? 
                    context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value : null;
        
        // 在实际应用中,可能需要将这些信息写入数据库或发送到分析服务
        _logger.LogInformation(
            "Download: {Path}, User: {User}, IP: {IP}, Range: {Start}-{End}, Size: {Size}, Complete: {Complete}",
            relativePath, userId ?? "anonymous", ipAddress, 
            range.Start, range.End, range.Length, !range.IsPartial);
    }
}

public static class SmartFileMiddlewareExtensions
{
    public static IServiceCollection AddSmartFileMiddleware(
        this IServiceCollection services,
        Action<SmartFileOptions> configureOptions = null)
    {
        var options = new SmartFileOptions();
        configureOptions?.Invoke(options);
        
        services.AddSingleton(options);
        
        return services;
    }
    
    public static IApplicationBuilder UseSmartFile(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SmartFileMiddleware>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807

注册方式:

// 在Startup.ConfigureServices中
services.AddSmartFileMiddleware(options => {
    // 基础配置
    options.FilesPath = Path.Combine(env.ContentRootPath, "Files");
    options.EnableRangeProcessing = true;
    options.EnableCompression = true;
    options.EnableETags = true;
    options.CacheDuration = TimeSpan.FromHours(24);
    
    // 安全配置
    options.EnableDirectoryBrowsing = true;
    options.ValidateFilenames = true;
    options.AllowCrossOrigin = true;
    
    // 允许的文件类型
    options.AllowedExtensions.AddRange(new[] { 
        ".jpg", ".jpeg", ".png", ".gif", 
        ".pdf", ".doc", ".docx", ".xls", ".xlsx",
        ".zip", ".txt", ".csv", ".mp4", ".mp3" 
    });
    
    // 可压缩的内容类型
    options.CompressibleTypes.AddRange(new[] {
        "text/plain", "text/html", "text/css",
        "application/javascript", "application/json",
        "application/xml", "text/csv"
    });
    
    // 允许访问的路径
    options.AllowedPaths.Add("/files/");
    options.AllowedPaths.Add("/downloads/");
    
    // 自定义授权处理程序
    options.AuthorizationHandler = (filePath, userName) => {
        // 简单的授权逻辑:受限目录只有已认证用户可访问
        if (filePath.StartsWith("restricted/", StringComparison.OrdinalIgnoreCase))
        {
            return !string.IsNullOrEmpty(userName);
        }
        return true;
    };
    
    // 文件下载跟踪
    options.TrackDownloads = true;
});

// 在Startup.Configure中
app.UseSmartFile();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

使用示例:

<!-- 普通文件链接 -->
<a href="/files/document.pdf">View PDF</a>

<!-- 强制下载 -->
<a href="/files/document.pdf?download=true">Download PDF</a>

<!-- 视频文件使用范围请求 -->
<video controls>
  <source src="/files/video.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>

<!-- 带有范围和压缩支持的大文件下载 -->
<a href="/files/large-dataset.csv">Download Large CSV</a>

<!-- 访问需要认证的文件 -->
<a href="/files/restricted/confidential.pdf">Confidential Document</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

应用场景: 文件服务器、下载中心、媒体流媒体应用、文档管理系统,需要支持大文件下载、断点续传和媒体流播放的应用。

市场集成: 与云存储服务(如Azure Blob Storage, AWS S3)集成,支持从云存储中提取文件;与内容分发网络集成,优化文件传输;与媒体转码服务集成,支持视频按需转码。

# 总结

以上20个ASP.NET Core中间件涵盖了各种实际应用场景,可以显著提升应用程序的功能性、性能和安全性。这些中间件都遵循模块化设计原则,可以独立使用或与其他中间件组合使用,以构建功能丰富的Web应用和API。

您可以根据项目需求选择适当的中间件,并根据具体场景进行自定义。每个中间件都可以进一步扩展和优化,以适应特定的业务需求。

使用这些中间件的关键优势在于它们提供了一种标准化且可重用的方式来处理常见的Web应用功能,避免了重复开发,并确保了最佳实践的应用。此外,由于中间件在ASP.NET Core请求处理管道中的位置可以灵活配置,您可以精确控制各种功能的执行顺序。

随着项目的发展,您可以持续改进和扩展这些中间件,或者开发新的中间件来满足新的需求。ASP.NET Core的中间件架构为应用程序的可维护性和可扩展性提供了坚实的基础。

编辑 (opens new window)
#asp.netcore
上次更新: 2026/03/05, 09:08:21
中间件基础

← 中间件基础

最近更新
01
鉴权服务中心
03-11
02
聚合根
03-11
03
补充
02-06
更多文章>
Theme by Vdoing | Copyright © 2019-2026 moklgy's blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式