using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Markdig;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using Microsoft.AspNetCore.Http;
using Microsoft.IO;
namespace Dpz.Core.Infrastructure;
public static class ApplicationTools
{
/// <summary>
/// 起始时间
/// </summary>
public static readonly DateTime StartTime = new(2018, 2, 11, 13, 56, 24, DateTimeKind.Local);
public static readonly Lazy<string> ApplicationVersion = new(() =>
Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "unknown"
);
public static readonly RecyclableMemoryStreamManager MemoryStreamManager =
new RecyclableMemoryStreamManager();
#region 通配符匹配算法
// 缓存匹配结果以提高性能,使用ConcurrentDictionary保证线程安全
private static readonly ConcurrentDictionary<
(string input, string pattern),
bool
> WildcardMatchCache = new();
// 缓存大小限制,避免内存泄漏
private const int MaxWildcardCacheSize = 10000;
/// <summary>
/// 通配符匹配
/// 支持 * (匹配任意字符) 和 ? (匹配单个字符)
/// 默认忽略大小写,包含缓存机制以提高重复匹配的性能
/// </summary>
/// <param name="input">要匹配的输入字符串</param>
/// <param name="pattern">通配符模式</param>
/// <param name="ignoreCase">是否忽略大小写,默认为 true</param>
/// <returns>是否匹配成功</returns>
/// <example>
/// <code>
/// bool result1 = ApplicationTools.WildcardMatch("/api/users/123", "/api/*"); // true
/// bool result2 = ApplicationTools.WildcardMatch("file.txt", "*.txt"); // true
/// bool result3 = ApplicationTools.WildcardMatch("test", "t??t"); // true
/// bool result4 = ApplicationTools.WildcardMatch("ABC", "abc"); // true (默认忽略大小写)
/// bool result5 = ApplicationTools.WildcardMatch("ABC", "abc", false); // false (区分大小写)
/// </code>
/// </example>
public static bool WildcardMatch(string input, string pattern, bool ignoreCase = true)
{
if (string.IsNullOrEmpty(input) && string.IsNullOrEmpty(pattern))
{
return true;
}
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(pattern))
{
// 空输入只能匹配全为 * 的模式
if (string.IsNullOrEmpty(input) && !string.IsNullOrEmpty(pattern))
{
return pattern.All(c => c == '*');
}
return false;
}
// 检查缓存
var cacheKey = ignoreCase
? (input.ToLowerInvariant(), pattern.ToLowerInvariant())
: (input, pattern);
if (WildcardMatchCache.TryGetValue(cacheKey, out var cachedResult))
{
return cachedResult;
}
// 执行匹配
var result = WildcardMatchCore(input.AsSpan(), pattern.AsSpan(), ignoreCase);
// 缓存结果(有大小限制)
if (WildcardMatchCache.Count < MaxWildcardCacheSize)
{
WildcardMatchCache.TryAdd(cacheKey, result);
}
else if (WildcardMatchCache.Count >= MaxWildcardCacheSize)
{
// 当缓存满时,清理一半的缓存
ClearHalfWildcardCache();
WildcardMatchCache.TryAdd(cacheKey, result);
}
return result;
}
/// <summary>
/// 清空通配符匹配缓存
/// 在内存压力较大或模式变化频繁时可以调用此方法
/// </summary>
public static void ClearWildcardCache()
{
WildcardMatchCache.Clear();
}
/// <summary>
/// 获取当前通配符匹配缓存的大小
/// </summary>
/// <returns>缓存中的项目数量</returns>
public static int GetWildcardCacheSize()
{
return WildcardMatchCache.Count;
}
/// <summary>
/// 将通配符模式转换为 .NET 正则表达式字符串:`*` 匹配任意字符、`?` 匹配单个字符,
/// 其余字符按字面量转义(正则元字符不会影响匹配)。
/// </summary>
/// <param name="pattern">通配符模式</param>
/// <param name="captureLiterals">
/// 为 true 时,被 `*`/`?` 分隔的连续字面量片段各自包在捕获组中,
/// 便于在匹配结果中定位实际命中的文本(如高亮)。
/// </param>
/// <returns>正则表达式字符串(不含首尾锚点,调用方自行决定是否锚定)</returns>
public static string WildcardToRegexPattern(string pattern, bool captureLiterals = false)
{
if (string.IsNullOrEmpty(pattern))
{
return "";
}
var regex = new StringBuilder();
var literalRun = new StringBuilder();
foreach (var c in pattern)
{
if (c is '*' or '?')
{
if (literalRun.Length > 0)
{
AppendLiteralRun(regex, literalRun, captureLiterals);
literalRun.Clear();
}
regex.Append(c == '*' ? ".*" : ".");
}
else
{
literalRun.Append(c);
}
}
if (literalRun.Length > 0)
{
AppendLiteralRun(regex, literalRun, captureLiterals);
}
return regex.ToString();
static void AppendLiteralRun(StringBuilder target, StringBuilder run, bool capture)
{
var escaped = Regex.Escape(run.ToString());
if (capture)
{
target.Append('(').Append(escaped).Append(')');
}
else
{
target.Append(escaped);
}
}
}
/// <summary>
/// 核心通配符匹配算法实现
/// </summary>
private static bool WildcardMatchCore(
ReadOnlySpan<char> input,
ReadOnlySpan<char> pattern,
bool ignoreCase
)
{
// 处理空字符串的特殊情况
if (pattern.Length == 0)
{
return input.Length == 0;
}
if (input.Length == 0)
{
// 输入为空,只有模式全部是 '*' 才匹配
for (var i = 0; i < pattern.Length; i++)
{
if (pattern[i] != '*')
{
return false;
}
}
return true;
}
var inputIndex = 0;
var patternIndex = 0;
var starIndex = -1;
var match = 0;
while (inputIndex < input.Length)
{
// 如果模式字符是 '?' 或者字符匹配
if (
patternIndex < pattern.Length
&& (
pattern[patternIndex] == '?'
|| CharEquals(input[inputIndex], pattern[patternIndex], ignoreCase)
)
)
{
inputIndex++;
patternIndex++;
}
// 如果模式字符是 '*'
else if (patternIndex < pattern.Length && pattern[patternIndex] == '*')
{
starIndex = patternIndex;
match = inputIndex;
patternIndex++;
}
// 如果之前遇到过 '*',回溯
else if (starIndex != -1)
{
patternIndex = starIndex + 1;
match++;
inputIndex = match;
}
// 不匹配
else
{
return false;
}
}
// 跳过模式末尾的 '*'
while (patternIndex < pattern.Length && pattern[patternIndex] == '*')
{
patternIndex++;
}
return patternIndex == pattern.Length;
}
/// <summary>
/// 字符比较,支持大小写敏感/不敏感
/// </summary>
private static bool CharEquals(char a, char b, bool ignoreCase)
{
return ignoreCase ? char.ToLowerInvariant(a) == char.ToLowerInvariant(b) : a == b;
}
/// <summary>
/// 清理一半的缓存以避免内存过度使用
/// </summary>
private static void ClearHalfWildcardCache()
{
try
{
var keysToRemove = WildcardMatchCache.Keys.Take(WildcardMatchCache.Count / 2).ToList();
foreach (var key in keysToRemove)
{
WildcardMatchCache.TryRemove(key, out _);
}
}
catch
{
// 如果清理失败,清空所有缓存
WildcardMatchCache.Clear();
}
}
#endregion
/// <summary>
/// Markdown转为Html
/// </summary>
/// <param name="markdown"></param>
/// <param name="disableHtml">是否禁用html(默认禁用)</param>
/// <returns></returns>
public static string MarkdownToHtml(this string markdown, bool disableHtml = true)
{
var pipelineBuild = new MarkdownPipelineBuilder()
.UseAutoLinks()
.UsePipeTables()
.UseTaskLists()
.UseEmphasisExtras()
.UseAutoIdentifiers();
if (disableHtml)
{
pipelineBuild.DisableHtml();
}
var pipeline = pipelineBuild.Build();
var document = Markdown.Parse(markdown, pipeline);
foreach (var link in document.Descendants<LinkInline>())
{
link.GetAttributes().AddPropertyIfNotExist("target", "_blank");
}
foreach (var link in document.Descendants<AutolinkInline>())
{
link.GetAttributes().AddPropertyIfNotExist("target", "_blank");
}
return document.ToHtml(pipeline);
}
/// <summary>
/// 失败重试
/// </summary>
/// <param name="func"></param>
/// <param name="retryInterval">重试间隔</param>
/// <param name="maxAttemptCount">最大重试次数</param>
/// <param name="specificExceptionTypes">指定异常</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="AggregateException"></exception>
public static async Task<T> RetryAsync<T>(
Func<Task<T>> func,
TimeSpan retryInterval,
int maxAttemptCount = 3,
ICollection<Type>? specificExceptionTypes = null
)
{
if (maxAttemptCount <= 0)
{
maxAttemptCount = 3;
}
var exceptions = new List<Exception>();
for (var attempted = 0; attempted < maxAttemptCount; attempted++)
{
try
{
if (attempted > 0)
{
await Task.Delay(retryInterval);
}
return await func();
}
catch (Exception ex)
{
if (specificExceptionTypes != null && specificExceptionTypes.Contains(ex.GetType()))
{
throw;
}
exceptions.Add(ex);
}
}
throw new AggregateException(
$"this task has been repeated {maxAttemptCount} times and has failed",
exceptions
);
}
/// <summary>
/// 失败重试(无返回值)
/// </summary>
/// <param name="func"></param>
/// <param name="retryInterval">重试间隔</param>
/// <param name="maxAttemptCount">最大重试次数</param>
/// <param name="specificExceptionTypes">指定异常</param>
/// <exception cref="AggregateException"></exception>
public static async Task RetryAsync(
Func<Task> func,
TimeSpan retryInterval,
int maxAttemptCount = 3,
ICollection<Type>? specificExceptionTypes = null
)
{
_ = await RetryAsync(
async () =>
{
await func();
return 0;
},
retryInterval,
maxAttemptCount,
specificExceptionTypes
);
}
/// <summary>
/// 获取IP地址
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static string GetIpAddress(this HttpRequest? request)
{
if (request == null)
{
return "unknown";
}
#if DEBUG
return "113.110.235.52";
#endif
#pragma warning disable CS0162 // 检测到不可到达的代码
return GetClientIp(request);
#pragma warning restore CS0162 // 检测到不可到达的代码
}
private static string GetClientIp(HttpRequest request)
{
// 优先从 Ali-Cdn-Real-Ip 中获取真实IP
var aliCdnRealIp = request.Headers["Ali-Cdn-Real-Ip"].FirstOrDefault();
if (!string.IsNullOrEmpty(aliCdnRealIp))
{
var ip = aliCdnRealIp.Trim();
if (
!string.IsNullOrEmpty(ip)
&& !ip.Equals("unknown", StringComparison.OrdinalIgnoreCase)
)
{
return ip;
}
}
var forwardedFor = request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(forwardedFor))
{
var ip = forwardedFor.Split(',')[0].Trim();
if (
!string.IsNullOrEmpty(ip)
&& !ip.Equals("unknown", StringComparison.OrdinalIgnoreCase)
)
{
return ip;
}
}
var realIp = request.Headers["X-Real-IP"].FirstOrDefault();
if (!string.IsNullOrEmpty(realIp))
{
return realIp;
}
return request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
#nullable enable
public static string GenerateSecret(int length = 32)
{
// ReSharper disable once StringLiteralTypo
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
ArgumentOutOfRangeException.ThrowIfLessThan(length, 1);
Span<byte> buffer = stackalloc byte[length];
RandomNumberGenerator.Fill(buffer);
var chars = new char[length];
for (var i = 0; i < length; i++)
{
chars[i] = alphabet[buffer[i] % alphabet.Length];
}
return new string(chars);
}
}
基于对该文件的阅读以及与其调用方、同目录文件、项目 README 的交叉检索,下面给出对 ApplicationTools.cs 的完整解读。
文件定位
ApplicationTools 是位于 Dpz.Core.Infrastructure(dpz.core 系统的公共基础组件库,参见 README.md)中的一个 静态工具类,本身不持有业务状态,只暴露一批跨项目通用的静态字段与扩展方法。它不依赖自身之外的服务,属于"零依赖基础设施",被 Web、Service、ObjectStorage、Hangfire/Jobs、Auth 等多个程序集直接引用。
静态字段
StartTime:固定的本地时间(2018-02-11),被当作"系统起始时间"常量使用(代码检索中未发现同项目其它地方直接引用它,主要作为对外统一时间基准)。ApplicationVersion:Lazy<string>,延迟读取入口程序集版本号,失败回退为"unknown"。调用方包括 LogConfiguration.cs(日志附加Version属性)、AspNetCoreServiceCollectionExtensions.cs 的/api/version接口、以及多个 cshtml 页面(如 _Layout.cshtml)展示当前版本。MemoryStreamManager:RecyclableMemoryStreamManager(Microsoft.IO 大对象内存池),用于替代大容量MemoryStream,避免 LOH 碎片。调用方包括 ExcelExtensions.cs(导出 Excel)、ParallelChunkBreakpointUpload.cs、S3ObjectStorageService.cs 等上传/传输场景。
各功能模块与协作关系
1. 通配符匹配(WildcardMatch 系列)
对外 API:
WildcardMatch(input, pattern, ignoreCase=true):支持*(任意长度)与?(单字符)的全串匹配。核心调用方是 InterceptRuleService.cs,它把拦截规则(UA、IP、URI、请求方法、查询参数、Header)的Pattern交给WildcardMatch判定。WildcardToRegexPattern(pattern, captureLiterals=false):把通配符模式转成正则字符串(*→.*、?→.,普通字符经Regex.Escape转义,不附带首尾锚点,由调用方决定锚定)。代码搜索场景在用它:如 CodeFileSystemEntryService.cs(含通配符时加^...$做 glob 全串匹配,否则子串匹配)与 _CodeSearchResultsPartial.cshtml(以captureLiterals: true输出捕获组,用于在结果中高亮命中片段)。
实现细节:
- 带
ConcurrentDictionary缓存(上限 10000 条,满时清掉约一半后重试写入),保证线程安全并减少重复计算;另提供ClearWildcardCache/GetWildcardCacheSize用于内存管理。 - 核心算法
WildcardMatchCore使用基于Span<char>的贪心 + 星号回溯匹配,无正则开销,且对空串边界有专门处理。
flowchart TD
A[WildcardMatch 入口] --> B{输入/模式均为空?}
B -- 是 --> C[返回 true]
B -- 否 --> D{其中一方为空?}
D -- 输入空、模式全是* --> E[返回 true]
D -- 其它一方为空 --> F[返回 false]
D -- 都不为空 --> G[计算缓存键]
G --> H{命中缓存?}
H -- 是 --> I[直接返回缓存结果]
H -- 否 --> J[WildcardMatchCore 逐字符扫描]
J --> K{字符相等 或 模式为?}
K -- 是 --> L[双指针前进]
K -- 否 --> M{模式为*?}
M -- 是 --> N[记录 starIndex 与 match, 只前进模式指针]
M -- 否 --> O{存在之前的*?}
O -- 是 --> P[回溯: 让 * 多吞一个字符]
O -- 否 --> Q[返回 false]
L --> R{输入扫描完?}
N --> R
P --> R
R -- 否 --> K
R -- 是 --> S[跳过模式尾部连续*]
S --> T{模式指针到底?}
T -- 是 --> U[返回 true]
T -- 否 --> V[返回 false]
U --> W[写入缓存并返回]
2. Markdown 渲染(MarkdownToHtml 扩展方法)
使用 Markdig 构建管道,启用自动链接、管道表格、任务列表、强调扩展、自动标题锚点;disableHtml 默认 true 会关闭原始 HTML(DisableHtml(),即 HTML 标签被转义输出,用于防 XSS 的评论区等),视图层显式传 false 表示允许嵌入 HTML。渲染前遍历 LinkInline 与 AutolinkInline,为所有链接补 target="_blank"。
该方法是全文里被引用最广的一个工具,典型场景:
- 渲染端:文章阅读页 Read.cshtml、首页 Markdown Markdown.cshtml、评论 _CommentTextPartial.cshtml、搜索结果 Search.cshtml;
- 提取图片/纯文本:多个 Mediator 处理器(如 EditMarkdownEvent.cs、MumbleImageMetadataCollectedHandler.cs)先转 HTML 再用 AngleSharp 解析出
<img>;邮件生成 EmailSenderHelper.cs 也是先转 HTML 再截图/发信。
3. 失败重试(RetryAsync,两个重载)
- 泛型版本
Task<T>:按maxAttemptCount循环执行,第 2 次起每次先Task.Delay(retryInterval);捕获异常后,若specificExceptionTypes指定了该异常类型则立即原样抛出(不重试),否则收集起来;全部失败后抛出带累计异常的AggregateException。 - 无返回值版本包装泛型版本(委托转成返回 0 的
Task<int>)。 - 调用方都是"外部网络 IO 易抖动"的场景,如 SteamGameService.cs(抓取 Steam 数据)、SafeFileService.cs(FTP/SFTP 上传)、TaskService.cs 与 ItHomeActivator.cs(下载/抓取)。
4. 获取客户端 IP(GetIpAddress 扩展)
取真实客户端 IP 的优先级链:Ali-Cdn-Real-Ip → X-Forwarded-For 第一段 → X-Real-IP → Connection.RemoteIpAddress,并对取值做 "unknown" 剔除与 trim。比较特殊的是 #if DEBUG 分支:Debug 编译下直接硬编码返回 113.110.235.52,并以 #pragma 压制后续不可达代码的警告(Release 才走真实逻辑)。调用方都是安全/审计类中间件:限流 RateLimitMiddleware.cs、反爬 RejectBotsMiddleware.cs 与 BotProtectionMiddleware.cs、登录/会话审计(AccountLoginHistoryService.cs、AccountSessionService.cs)以及拦截规则 InterceptRuleService.cs。
5. 密钥生成(GenerateSecret)
用 RandomNumberGenerator.Fill 填充 stackalloc 缓冲,再按 alphabet[buffer[i] % 62] 映射出只含大小写字母和数字的随机串(length<1 时抛 ArgumentOutOfRangeException)。在全仓库源码检索中,除本文件外没有其它引用点——说明它当前属于"待接入"的通用工具 API(调用关系未能确认,不臆断)。
与同目录其它工具的差异补充
同目录的 DpzAppBuilderExtensions.cs 也定义了一个字符串扩展 WildCardMatch(先做相等判断再递归拆 * 的简化实现),与 ApplicationTools.WildcardMatch 是两套并存、实现各异的通配匹配逻辑(后者更通用、带缓存且处理 ? 与忽略大小写),使用时需要注意区分。另外 Dpz.Core.Simple.Test 下存在针对本文件通配算法的单元/性能对比测试(ApplicationToolsWildcardTest.cs、WildcardPerformanceTest.cs)。
小结
一句话概括:ApplicationTools 是一个无状态的静态基础设施工具箱,集中提供了通配匹配(带缓存/正则转换)、Markdown 渲染、异步重试、真实 IP 探测、安全随机串生成以及应用元信息(版本、起始时间、可回收内存流)这些被 Web/Service/Jobs/对象存储各层复用的横切能力,并通过扩展方法(MarkdownToHtml、GetIpAddress)融入既有调用链。文件内部没有复杂的服务编排,因此除通配匹配核心算法外不存在需要额外说明的执行流程。