A:common基础功能及测试
This commit is contained in:
1 parent
9ffa70acde
commit
237cbd8788
11 files changed
+5929
-32
No files matched your search
@@ -0,0 +1,478 @@
|
|||||||
|
namespace Common.ExpandFun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页结果类
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据项类型</typeparam>
|
||||||
|
public class PagedResult<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页码(从 1 开始)
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页大小
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总记录数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总页数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页数据
|
||||||
|
/// </summary>
|
||||||
|
public List<T> Items { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否有上一页
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPreviousPage => PageIndex > 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否有下一页
|
||||||
|
/// </summary>
|
||||||
|
public bool HasNextPage => PageIndex < TotalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 树形节点类
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">节点数据类型</typeparam>
|
||||||
|
public class TreeNode<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 节点数据
|
||||||
|
/// </summary>
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 父节点 ID
|
||||||
|
/// </summary>
|
||||||
|
public object? ParentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前节点 ID
|
||||||
|
/// </summary>
|
||||||
|
public object Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 子节点列表
|
||||||
|
/// </summary>
|
||||||
|
public List<TreeNode<T>> Children { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 层级深度(根节点为 0)
|
||||||
|
/// </summary>
|
||||||
|
public int Level { get; set; }
|
||||||
|
|
||||||
|
public TreeNode(T data, object id, object? parentId = null)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
Id = id;
|
||||||
|
ParentId = parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 集合扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class CollectionExpand
|
||||||
|
{
|
||||||
|
#region 分页处理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将集合转换为分页结果
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">元素类型</typeparam>
|
||||||
|
/// <param name="source">源集合</param>
|
||||||
|
/// <param name="pageIndex">页码(从 1 开始)</param>
|
||||||
|
/// <param name="pageSize">每页大小</param>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
public static PagedResult<T> ToPagedResult<T>(this IEnumerable<T> source, int pageIndex, int pageSize)
|
||||||
|
{
|
||||||
|
if (pageIndex < 1)
|
||||||
|
pageIndex = 1;
|
||||||
|
|
||||||
|
if (pageSize < 1)
|
||||||
|
pageSize = 10;
|
||||||
|
|
||||||
|
var list = source as IList<T> ?? source.ToList();
|
||||||
|
var totalCount = list.Count;
|
||||||
|
|
||||||
|
var items = list
|
||||||
|
.Skip((pageIndex - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new PagedResult<T>
|
||||||
|
{
|
||||||
|
PageIndex = pageIndex,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 IQueryable 转换为分页结果(适用于数据库查询)
|
||||||
|
/// </summary>
|
||||||
|
public static PagedResult<T> ToPagedResult<T>(this IQueryable<T> source, int pageIndex, int pageSize)
|
||||||
|
{
|
||||||
|
if (pageIndex < 1)
|
||||||
|
pageIndex = 1;
|
||||||
|
|
||||||
|
if (pageSize < 1)
|
||||||
|
pageSize = 10;
|
||||||
|
|
||||||
|
var totalCount = source.Count();
|
||||||
|
|
||||||
|
var items = source
|
||||||
|
.Skip((pageIndex - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new PagedResult<T>
|
||||||
|
{
|
||||||
|
PageIndex = pageIndex,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 树形结构转换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将平铺列表转换为树形结构
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">元素类型</typeparam>
|
||||||
|
/// <param name="source">源集合</param>
|
||||||
|
/// <param name="idSelector">ID 选择器</param>
|
||||||
|
/// <param name="parentIdSelector">父 ID 选择器</param>
|
||||||
|
/// <param name="rootParentId">根节点的父 ID(默认为 null)</param>
|
||||||
|
/// <returns>树形结构</returns>
|
||||||
|
public static List<TreeNode<T>> ToTree<T>(
|
||||||
|
this IEnumerable<T> source,
|
||||||
|
Func<T, object> idSelector,
|
||||||
|
Func<T, object?> parentIdSelector,
|
||||||
|
object? rootParentId = null)
|
||||||
|
{
|
||||||
|
var list = source.ToList();
|
||||||
|
var nodes = list.Select(item => new TreeNode<T>(item, idSelector(item), parentIdSelector(item))).ToList();
|
||||||
|
var lookup = nodes.ToLookup(n => n.ParentId);
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
node.Children = lookup[node.Id].ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建层级深度
|
||||||
|
void SetLevel(TreeNode<T> node, int level)
|
||||||
|
{
|
||||||
|
node.Level = level;
|
||||||
|
foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
SetLevel(child, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootNodes = nodes.Where(n => Equals(n.ParentId, rootParentId)).ToList();
|
||||||
|
foreach (var root in rootNodes)
|
||||||
|
{
|
||||||
|
SetLevel(root, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将树形结构转换为平铺列表
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">元素类型</typeparam>
|
||||||
|
/// <param name="tree">树形结构</param>
|
||||||
|
/// <returns>平铺列表</returns>
|
||||||
|
public static List<T> ToFlatList<T>(this IEnumerable<TreeNode<T>> tree)
|
||||||
|
{
|
||||||
|
var result = new List<T>();
|
||||||
|
|
||||||
|
void Traverse(IEnumerable<TreeNode<T>> nodes)
|
||||||
|
{
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
result.Add(node.Data);
|
||||||
|
if (node.Children.Any())
|
||||||
|
{
|
||||||
|
Traverse(node.Children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Traverse(tree);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 遍历树形结构(深度优先)
|
||||||
|
/// </summary>
|
||||||
|
public static void TraverseTree<T>(this IEnumerable<TreeNode<T>> tree, Action<TreeNode<T>> action)
|
||||||
|
{
|
||||||
|
void Traverse(IEnumerable<TreeNode<T>> nodes)
|
||||||
|
{
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
action(node);
|
||||||
|
if (node.Children.Any())
|
||||||
|
{
|
||||||
|
Traverse(node.Children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Traverse(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 批处理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将集合分批处理
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">元素类型</typeparam>
|
||||||
|
/// <param name="source">源集合</param>
|
||||||
|
/// <param name="batchSize">批次大小</param>
|
||||||
|
/// <returns>批次集合</returns>
|
||||||
|
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
|
||||||
|
{
|
||||||
|
if (batchSize <= 0)
|
||||||
|
throw new ArgumentException("Batch size must be greater than 0", nameof(batchSize));
|
||||||
|
|
||||||
|
var batch = new List<T>(batchSize);
|
||||||
|
|
||||||
|
foreach (var item in source)
|
||||||
|
{
|
||||||
|
batch.Add(item);
|
||||||
|
|
||||||
|
if (batch.Count == batchSize)
|
||||||
|
{
|
||||||
|
yield return batch;
|
||||||
|
batch = new List<T>(batchSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count > 0)
|
||||||
|
{
|
||||||
|
yield return batch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分批执行操作
|
||||||
|
/// </summary>
|
||||||
|
public static void ForEachBatch<T>(this IEnumerable<T> source, int batchSize, Action<IEnumerable<T>> action)
|
||||||
|
{
|
||||||
|
foreach (var batch in source.Batch(batchSize))
|
||||||
|
{
|
||||||
|
action(batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 空集合安全操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 如果集合为空,返回默认集合
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T>? source)
|
||||||
|
{
|
||||||
|
return source ?? Enumerable.Empty<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 如果集合为 null 或空,返回默认值
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> OrDefault<T>(this IEnumerable<T>? source, IEnumerable<T> defaultValue)
|
||||||
|
{
|
||||||
|
var list = source?.ToList();
|
||||||
|
return list == null || list.Count == 0 ? defaultValue : list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断集合是否为 null 或空
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source)
|
||||||
|
{
|
||||||
|
return source == null || !source.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断集合是否有元素
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasItems<T>(this IEnumerable<T>? source)
|
||||||
|
{
|
||||||
|
return source != null && source.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 其他实用扩展
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对集合中的每个元素执行操作
|
||||||
|
/// </summary>
|
||||||
|
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
|
||||||
|
{
|
||||||
|
foreach (var item in source)
|
||||||
|
{
|
||||||
|
action(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对集合中的每个元素执行操作(带索引)
|
||||||
|
/// </summary>
|
||||||
|
public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (var item in source)
|
||||||
|
{
|
||||||
|
action(item, index++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接字符串(带分隔符)
|
||||||
|
/// </summary>
|
||||||
|
public static string JoinString<T>(this IEnumerable<T> source, string separator = ", ")
|
||||||
|
{
|
||||||
|
return string.Join(separator, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 随机打乱集合顺序
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
var random = new Random();
|
||||||
|
return source.OrderBy(_ => random.Next());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 随机获取 n 个元素
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> source, int count)
|
||||||
|
{
|
||||||
|
return source.Shuffle().Take(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将集合分成多组(平均分配)
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int parts)
|
||||||
|
{
|
||||||
|
if (parts <= 0)
|
||||||
|
throw new ArgumentException("Parts must be greater than 0", nameof(parts));
|
||||||
|
|
||||||
|
var list = source.ToList();
|
||||||
|
var batchSize = (int)Math.Ceiling(list.Count / (double)parts);
|
||||||
|
|
||||||
|
return list.Batch(batchSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断集合中是否所有元素都唯一
|
||||||
|
/// </summary>
|
||||||
|
public static bool AllUnique<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
var set = new HashSet<T>();
|
||||||
|
return source.All(set.Add);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断集合中是否有重复元素
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasDuplicates<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
return !source.AllUnique();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取集合中的重复元素
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> GetDuplicates<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
return source.GroupBy(x => x)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将两个集合交替合并
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> Interleave<T>(this IEnumerable<T> first, IEnumerable<T> second)
|
||||||
|
{
|
||||||
|
using var e1 = first.GetEnumerator();
|
||||||
|
using var e2 = second.GetEnumerator();
|
||||||
|
|
||||||
|
while (e1.MoveNext() && e2.MoveNext())
|
||||||
|
{
|
||||||
|
yield return e1.Current;
|
||||||
|
yield return e2.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (e1.MoveNext())
|
||||||
|
yield return e1.Current;
|
||||||
|
|
||||||
|
while (e2.MoveNext())
|
||||||
|
yield return e2.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 安全地获取指定索引的元素(索引越界返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static T? ElementAtOrDefault<T>(this IEnumerable<T> source, int index, T? defaultValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return source.ElementAtOrDefault(index) ?? defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将集合中的 null 元素过滤掉
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
|
||||||
|
{
|
||||||
|
return source.Where(x => x != null)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对集合进行分组并转换为字典
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<TKey, List<TValue>> ToDictionaryList<TSource, TKey, TValue>(
|
||||||
|
this IEnumerable<TSource> source,
|
||||||
|
Func<TSource, TKey> keySelector,
|
||||||
|
Func<TSource, TValue> valueSelector) where TKey : notnull
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.GroupBy(keySelector)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Select(valueSelector).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,58 +1,712 @@
|
|||||||
namespace Common.ExpandFun;
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Common.ExpandFun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字符串扩展方法
|
||||||
|
/// 提供字符串验证、转换、截取、格式化等实用功能
|
||||||
|
/// </summary>
|
||||||
public static class StringExpand
|
public static class StringExpand
|
||||||
{
|
{
|
||||||
/// <summary>
|
#region 类型转换
|
||||||
/// 检测是否是 url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="str"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool IsValidUrl(this string str)
|
|
||||||
{
|
|
||||||
return Uri.TryCreate(str, UriKind.Absolute, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为整数(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
public static int ToInt(this string? str, int defaultValue = default)
|
public static int ToInt(this string? str, int defaultValue = default)
|
||||||
{
|
{
|
||||||
return TryToInt(str) ?? defaultValue;
|
return TryToInt(str) ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为整数
|
||||||
|
/// </summary>
|
||||||
public static int? TryToInt(this string? str)
|
public static int? TryToInt(this string? str)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(str))
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
return int.TryParse(str, out var result) ? result : null;
|
||||||
{
|
|
||||||
return int.Parse(str);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为长整数(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static long ToLong(this string? str, long defaultValue = default)
|
||||||
|
{
|
||||||
|
return TryToLong(str) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为长整数
|
||||||
|
/// </summary>
|
||||||
|
public static long? TryToLong(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return long.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为浮点数(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
public static double ToDouble(this string? str, double defaultValue = default)
|
public static double ToDouble(this string? str, double defaultValue = default)
|
||||||
{
|
{
|
||||||
return TryToDouble(str) ?? defaultValue;
|
return TryToDouble(str) ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为浮点数
|
||||||
|
/// </summary>
|
||||||
public static double? TryToDouble(this string? str)
|
public static double? TryToDouble(this string? str)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(str))
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
return double.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 Decimal(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static decimal ToDecimal(this string? str, decimal defaultValue = default)
|
||||||
|
{
|
||||||
|
return TryToDecimal(str) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为 Decimal
|
||||||
|
/// </summary>
|
||||||
|
public static decimal? TryToDecimal(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return decimal.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为布尔值(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static bool ToBool(this string? str, bool defaultValue = default)
|
||||||
|
{
|
||||||
|
return TryToBool(str) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为布尔值
|
||||||
|
/// </summary>
|
||||||
|
public static bool? TryToBool(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return bool.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 DateTime(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ToDateTime(this string? str, DateTime defaultValue = default)
|
||||||
|
{
|
||||||
|
return TryToDateTime(str) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为 DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime? TryToDateTime(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return DateTime.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 Guid(失败返回默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static Guid ToGuid(this string? str, Guid defaultValue = default)
|
||||||
|
{
|
||||||
|
return TryToGuid(str) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试转换为 Guid
|
||||||
|
/// </summary>
|
||||||
|
public static Guid? TryToGuid(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Guid.TryParse(str, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串判断
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否为 null 或空字符串
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNullOrEmpty(this string? str)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否为 null、空字符串或仅包含空白字符
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNullOrWhiteSpace(this string? str)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否有值(非 null 且非空)
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasValue(this string? str)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否只包含数字
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNumeric(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.All(char.IsDigit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否只包含字母
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsAlpha(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.All(char.IsLetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否只包含字母和数字
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsAlphaNumeric(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.All(char.IsLetterOrDigit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测是否是有效的 URL
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsValidUrl(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Uri.TryCreate(str, UriKind.Absolute, out var uri)
|
||||||
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串截取和处理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 安全截取字符串(不会抛出异常)
|
||||||
|
/// </summary>
|
||||||
|
public static string SafeSubstring(this string? str, int startIndex, int? length = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (startIndex >= str.Length)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (startIndex < 0)
|
||||||
|
startIndex = 0;
|
||||||
|
|
||||||
|
if (length == null)
|
||||||
|
return str.Substring(startIndex);
|
||||||
|
|
||||||
|
var actualLength = Math.Min(length.Value, str.Length - startIndex);
|
||||||
|
return actualLength <= 0 ? string.Empty : str.Substring(startIndex, actualLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 截取字符串并添加省略号(超过指定长度时)
|
||||||
|
/// </summary>
|
||||||
|
public static string Truncate(this string? str, int maxLength, string suffix = "...")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (str.Length <= maxLength)
|
||||||
|
return str;
|
||||||
|
|
||||||
|
return str.Substring(0, maxLength) + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除字符串前后的指定字符串
|
||||||
|
/// </summary>
|
||||||
|
public static string Trim(this string? str, string trimString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString))
|
||||||
|
return str ?? string.Empty;
|
||||||
|
|
||||||
|
return str.TrimStart(trimString).TrimEnd(trimString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除字符串开头的指定字符串
|
||||||
|
/// </summary>
|
||||||
|
public static string TrimStart(this string? str, string trimString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString))
|
||||||
|
return str ?? string.Empty;
|
||||||
|
|
||||||
|
while (str.StartsWith(trimString))
|
||||||
|
{
|
||||||
|
str = str.Substring(trimString.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除字符串末尾的指定字符串
|
||||||
|
/// </summary>
|
||||||
|
public static string TrimEnd(this string? str, string trimString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString))
|
||||||
|
return str ?? string.Empty;
|
||||||
|
|
||||||
|
while (str.EndsWith(trimString))
|
||||||
|
{
|
||||||
|
str = str.Substring(0, str.Length - trimString.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除所有空白字符(包括空格、制表符、换行符等)
|
||||||
|
/// </summary>
|
||||||
|
public static string RemoveWhiteSpace(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反转字符串
|
||||||
|
/// </summary>
|
||||||
|
public static string Reverse(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var chars = str.ToCharArray();
|
||||||
|
Array.Reverse(chars);
|
||||||
|
return new string(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串格式化
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为驼峰命名(camelCase)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToCamelCase(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var words = str.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (words.Length == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.Append(words[0].ToLowerInvariant());
|
||||||
|
|
||||||
|
for (int i = 1; i < words.Length; i++)
|
||||||
|
{
|
||||||
|
result.Append(char.ToUpperInvariant(words[i][0]));
|
||||||
|
if (words[i].Length > 1)
|
||||||
|
result.Append(words[i].Substring(1).ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为帕斯卡命名(PascalCase)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToPascalCase(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var words = str.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var result = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var word in words)
|
||||||
|
{
|
||||||
|
result.Append(char.ToUpperInvariant(word[0]));
|
||||||
|
if (word.Length > 1)
|
||||||
|
result.Append(word.Substring(1).ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为蛇形命名(snake_case)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSnakeCase(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Regex.Replace(str, "([a-z])([A-Z])", "$1_$2").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为烤串命名(kebab-case)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToKebabCase(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首字母大写
|
||||||
|
/// </summary>
|
||||||
|
public static string Capitalize(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return char.ToUpperInvariant(str[0]) + (str.Length > 1 ? str.Substring(1) : string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首字母小写
|
||||||
|
/// </summary>
|
||||||
|
public static string Uncapitalize(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return char.ToLowerInvariant(str[0]) + (str.Length > 1 ? str.Substring(1) : string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串编码
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 编码
|
||||||
|
/// </summary>
|
||||||
|
public static string ToBase64(this string? str, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = (encoding ?? Encoding.UTF8).GetBytes(str);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 解码
|
||||||
|
/// </summary>
|
||||||
|
public static string FromBase64(this string? str, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return double.Parse(str);
|
var bytes = Convert.FromBase64String(str);
|
||||||
|
return (encoding ?? Encoding.UTF8).GetString(bytes);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch
|
||||||
{
|
{
|
||||||
return null;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL 编码
|
||||||
|
/// </summary>
|
||||||
|
public static string UrlEncode(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Uri.EscapeDataString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL 解码
|
||||||
|
/// </summary>
|
||||||
|
public static string UrlDecode(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Uri.UnescapeDataString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串哈希
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 MD5 哈希
|
||||||
|
/// </summary>
|
||||||
|
public static string ToMd5(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
var hash = md5.ComputeHash(bytes);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 SHA256 哈希
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSha256(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
var hash = sha256.ComputeHash(bytes);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串匹配
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否匹配指定的正则表达式
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsMatch(this string? str, string pattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Regex.IsMatch(str, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否包含指定的子串(忽略大小写)
|
||||||
|
/// </summary>
|
||||||
|
public static bool ContainsIgnoreCase(this string? str, string value)
|
||||||
|
{
|
||||||
|
if (str == null || value == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否以指定的子串开始(忽略大小写)
|
||||||
|
/// </summary>
|
||||||
|
public static bool StartsWithIgnoreCase(this string? str, string value)
|
||||||
|
{
|
||||||
|
if (str == null || value == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.StartsWith(value, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否以指定的子串结束(忽略大小写)
|
||||||
|
/// </summary>
|
||||||
|
public static bool EndsWithIgnoreCase(this string? str, string value)
|
||||||
|
{
|
||||||
|
if (str == null || value == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.EndsWith(value, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否等于指定的值(忽略大小写)
|
||||||
|
/// </summary>
|
||||||
|
public static bool EqualsIgnoreCase(this string? str, string? value)
|
||||||
|
{
|
||||||
|
if (str == null && value == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (str == null || value == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return str.Equals(value, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串替换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换字符串(忽略大小写)
|
||||||
|
/// </summary>
|
||||||
|
public static string ReplaceIgnoreCase(this string? str, string oldValue, string newValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(oldValue))
|
||||||
|
return str ?? string.Empty;
|
||||||
|
|
||||||
|
return Regex.Replace(str, Regex.Escape(oldValue), newValue ?? string.Empty, RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除字符串中的 HTML 标签
|
||||||
|
/// </summary>
|
||||||
|
public static string RemoveHtmlTags(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Regex.Replace(str, "<.*?>", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除字符串中的特殊字符(只保留字母、数字和空格)
|
||||||
|
/// </summary>
|
||||||
|
public static string RemoveSpecialCharacters(this string? str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Regex.Replace(str, "[^a-zA-Z0-9 ]", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 其他实用方法
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重复字符串 N 次
|
||||||
|
/// </summary>
|
||||||
|
public static string Repeat(this string? str, int count)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || count <= 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var sb = new StringBuilder(str.Length * count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sb.Append(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确保字符串以指定的前缀开始
|
||||||
|
/// </summary>
|
||||||
|
public static string EnsureStartsWith(this string? str, string prefix)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return prefix ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(prefix))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
return str.StartsWith(prefix) ? str : prefix + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确保字符串以指定的后缀结束
|
||||||
|
/// </summary>
|
||||||
|
public static string EnsureEndsWith(this string? str, string suffix)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return suffix ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(suffix))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
return str.EndsWith(suffix) ? str : str + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 如果字符串为 null 或空,返回默认值
|
||||||
|
/// </summary>
|
||||||
|
public static string OrDefault(this string? str, string defaultValue = "")
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(str) ? defaultValue : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取字符串的字节长度(UTF-8 编码)
|
||||||
|
/// </summary>
|
||||||
|
public static int GetByteLength(this string? str, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return (encoding ?? Encoding.UTF8).GetByteCount(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算字符串的相似度(Levenshtein 距离)
|
||||||
|
/// </summary>
|
||||||
|
public static int LevenshteinDistance(this string? source, string? target)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(source))
|
||||||
|
return target?.Length ?? 0;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(target))
|
||||||
|
return source.Length;
|
||||||
|
|
||||||
|
int[,] distance = new int[source.Length + 1, target.Length + 1];
|
||||||
|
|
||||||
|
for (int i = 0; i <= source.Length; i++)
|
||||||
|
distance[i, 0] = i;
|
||||||
|
|
||||||
|
for (int j = 0; j <= target.Length; j++)
|
||||||
|
distance[0, j] = j;
|
||||||
|
|
||||||
|
for (int i = 1; i <= source.Length; i++)
|
||||||
|
{
|
||||||
|
for (int j = 1; j <= target.Length; j++)
|
||||||
|
{
|
||||||
|
int cost = (target[j - 1] == source[i - 1]) ? 0 : 1;
|
||||||
|
distance[i, j] = Math.Min(
|
||||||
|
Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
|
||||||
|
distance[i - 1, j - 1] + cost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance[source.Length, target.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
+128
-1
@@ -11,6 +11,9 @@
|
|||||||
- ✅ **日期时间扩展** - 日期格式化、时间戳转换、时区转换、工作日计算
|
- ✅ **日期时间扩展** - 日期格式化、时间戳转换、时区转换、工作日计算
|
||||||
- ✅ **加密工具** - AES/RSA加密、MD5/SHA256哈希、Base64编解码、密码哈希、HMAC签名
|
- ✅ **加密工具** - AES/RSA加密、MD5/SHA256哈希、Base64编解码、密码哈希、HMAC签名
|
||||||
- ✅ **验证工具** - 邮箱、手机号、身份证、URL验证,与Result模式无缝集成
|
- ✅ **验证工具** - 邮箱、手机号、身份证、URL验证,与Result模式无缝集成
|
||||||
|
- ✅ **集合扩展** - 分页处理、树形结构转换、批处理、DistinctBy等高级集合操作
|
||||||
|
- ✅ **枚举工具** - 枚举解析、描述获取、标志位操作、下拉列表数据生成
|
||||||
|
- ✅ **反射工具** - 高性能属性访问(带缓存)、类型扫描、特性读取、对象映射
|
||||||
- ✅ **JSON 序列化** - 预配置的 JSON 序列化,支持 camelCase 和 IPAddress
|
- ✅ **JSON 序列化** - 预配置的 JSON 序列化,支持 camelCase 和 IPAddress
|
||||||
- ✅ **系统工具** - 动态端口分配、管理员权限检查
|
- ✅ **系统工具** - 动态端口分配、管理员权限检查
|
||||||
|
|
||||||
@@ -260,6 +263,118 @@ var result = Result.Failure<string>(exception);
|
|||||||
Console.WriteLine(result.ErrorCode); // 1001
|
Console.WriteLine(result.ErrorCode); // 1001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 10. 集合扩展
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
var items = Enumerable.Range(1, 100).ToList();
|
||||||
|
var pagedResult = items.ToPagedResult(pageIndex: 1, pageSize: 10);
|
||||||
|
Console.WriteLine($"Total pages: {pagedResult.TotalPages}");
|
||||||
|
|
||||||
|
// 树形结构转换
|
||||||
|
var departments = new List<Department>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "总公司", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "技术部", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "前端组", ParentId = 2 }
|
||||||
|
};
|
||||||
|
var tree = departments.ToTree(x => x.Id, x => x.ParentId);
|
||||||
|
|
||||||
|
// 批处理
|
||||||
|
var largeList = Enumerable.Range(1, 1000).ToList();
|
||||||
|
largeList.ForEachBatch(100, batch =>
|
||||||
|
{
|
||||||
|
// 每批处理 100 条记录
|
||||||
|
ProcessBatch(batch);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 去重(自定义键)
|
||||||
|
var users = new[] { user1, user2, user3 };
|
||||||
|
var uniqueUsers = users.DistinctBy(u => u.Email);
|
||||||
|
|
||||||
|
// 安全操作
|
||||||
|
List<int>? nullList = null;
|
||||||
|
var safeList = nullList.OrEmpty(); // 返回空集合而非 null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 11. 枚举工具
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
// 定义枚举
|
||||||
|
public enum OrderStatus
|
||||||
|
{
|
||||||
|
[Description("待支付")]
|
||||||
|
Pending = 0,
|
||||||
|
|
||||||
|
[Description("已支付")]
|
||||||
|
Paid = 1,
|
||||||
|
|
||||||
|
[Description("已完成")]
|
||||||
|
Completed = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举解析
|
||||||
|
var status = EnumHelper.Parse<OrderStatus>("Paid");
|
||||||
|
var statusFromDesc = EnumHelper.ParseFromDescription<OrderStatus>("已支付");
|
||||||
|
|
||||||
|
// 获取描述
|
||||||
|
string desc = OrderStatus.Paid.GetDescription(); // "已支付"
|
||||||
|
|
||||||
|
// 获取下拉列表选项
|
||||||
|
var options = EnumHelper.GetOptions<OrderStatus>();
|
||||||
|
// 返回 [{ Value: 0, Name: "Pending", Description: "待支付" }, ...]
|
||||||
|
|
||||||
|
// 标志位枚举操作
|
||||||
|
[Flags]
|
||||||
|
public enum Permissions
|
||||||
|
{
|
||||||
|
Read = 1,
|
||||||
|
Write = 2,
|
||||||
|
Delete = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions = Permissions.Read | Permissions.Write;
|
||||||
|
bool canRead = permissions.HasFlag(Permissions.Read); // true
|
||||||
|
var newPermissions = permissions.AddFlag(Permissions.Delete);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 12. 反射工具
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
// 高性能属性访问(使用表达式树和缓存)
|
||||||
|
var user = new User { Name = "Alice", Age = 25 };
|
||||||
|
var name = ReflectionHelper.GetPropertyValue(user, "Name");
|
||||||
|
ReflectionHelper.SetPropertyValue(user, "Age", 30);
|
||||||
|
|
||||||
|
// 类型扫描
|
||||||
|
var implementations = ReflectionHelper.FindImplementationsOf<IService>(assembly);
|
||||||
|
var typesWithAttribute = ReflectionHelper.FindTypesWithAttribute<MyAttribute>(assembly);
|
||||||
|
|
||||||
|
// 属性复制(对象映射)
|
||||||
|
var userDto = ReflectionHelper.MapTo<User, UserDto>(user);
|
||||||
|
|
||||||
|
// 特性读取(带缓存)
|
||||||
|
var attribute = ReflectionHelper.GetAttribute<DescriptionAttribute>(typeof(User));
|
||||||
|
bool hasAttribute = ReflectionHelper.HasAttribute<ObsoleteAttribute>(memberInfo);
|
||||||
|
|
||||||
|
// 创建实例
|
||||||
|
var instance = ReflectionHelper.CreateInstance<MyClass>();
|
||||||
|
|
||||||
|
// 方法调用
|
||||||
|
var result = ReflectionHelper.InvokeMethod(obj, "Calculate", 10, 20);
|
||||||
|
|
||||||
|
// 类型判断
|
||||||
|
bool isNumeric = ReflectionHelper.IsNumericType(typeof(int));
|
||||||
|
bool isCollection = ReflectionHelper.IsCollectionType(typeof(List<int>));
|
||||||
|
```
|
||||||
|
|
||||||
## 详细文档
|
## 详细文档
|
||||||
|
|
||||||
### Global - 依赖注入容器
|
### Global - 依赖注入容器
|
||||||
@@ -613,6 +728,12 @@ result
|
|||||||
- `ResultTests.cs` - Result 模式测试(包含 LINQ 和异步测试)
|
- `ResultTests.cs` - Result 模式测试(包含 LINQ 和异步测试)
|
||||||
- `ExceptionExpandTests.cs` - 自定义异常测试
|
- `ExceptionExpandTests.cs` - 自定义异常测试
|
||||||
- `StringExpandTests.cs` - 字符串扩展测试
|
- `StringExpandTests.cs` - 字符串扩展测试
|
||||||
|
- `DateTimeExpandTests.cs` - 日期时间扩展测试
|
||||||
|
- `CryptoHelperTests.cs` - 加密工具测试
|
||||||
|
- `ValidationHelperTests.cs` - 验证工具测试
|
||||||
|
- `CollectionExpandTests.cs` - 集合扩展测试
|
||||||
|
- `EnumHelperTests.cs` - 枚举工具测试
|
||||||
|
- `ReflectionHelperTests.cs` - 反射工具测试
|
||||||
- `JsonHelperTests.cs` - JSON 序列化测试
|
- `JsonHelperTests.cs` - JSON 序列化测试
|
||||||
- `SystemHelperTests.cs` - 系统工具测试
|
- `SystemHelperTests.cs` - 系统工具测试
|
||||||
|
|
||||||
@@ -665,11 +786,17 @@ Sharp/Common/
|
|||||||
│ ├── ExceptionExpand.cs # 自定义异常
|
│ ├── ExceptionExpand.cs # 自定义异常
|
||||||
│ └── ExampleExceptionEnum.cs # 预定义异常
|
│ └── ExampleExceptionEnum.cs # 预定义异常
|
||||||
├── ExpandFun/
|
├── ExpandFun/
|
||||||
│ └── StringExpand.cs # 字符串扩展
|
│ ├── StringExpand.cs # 字符串扩展
|
||||||
|
│ ├── DateTimeExpand.cs # 日期时间扩展
|
||||||
|
│ └── CollectionExpand.cs # 集合扩展
|
||||||
├── Results/
|
├── Results/
|
||||||
│ ├── Result.cs # Result 核心类
|
│ ├── Result.cs # Result 核心类
|
||||||
│ └── ResultExtensions.cs # Result 扩展方法
|
│ └── ResultExtensions.cs # Result 扩展方法
|
||||||
└── Utils/
|
└── Utils/
|
||||||
|
├── CryptoHelper.cs # 加密工具
|
||||||
|
├── ValidationHelper.cs # 验证工具
|
||||||
|
├── EnumHelper.cs # 枚举工具
|
||||||
|
├── ReflectionHelper.cs # 反射工具
|
||||||
├── JsonHelper.cs # JSON 工具
|
├── JsonHelper.cs # JSON 工具
|
||||||
└── SystemHelper.cs # 系统工具
|
└── SystemHelper.cs # 系统工具
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,526 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加密工具类
|
||||||
|
/// 提供 AES、RSA、哈希、Base64、密码哈希、HMAC 等加密功能
|
||||||
|
/// </summary>
|
||||||
|
public static class CryptoHelper
|
||||||
|
{
|
||||||
|
#region AES 加密解密
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AES 加密(使用密码派生密钥)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plainText">明文</param>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <returns>Base64 编码的密文(包含 Salt 和 IV)</returns>
|
||||||
|
public static string AesEncrypt(string plainText, string password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plainText))
|
||||||
|
throw new ArgumentNullException(nameof(plainText));
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
throw new ArgumentNullException(nameof(password));
|
||||||
|
|
||||||
|
// 生成随机 Salt
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||||
|
|
||||||
|
// 从密码派生密钥
|
||||||
|
using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256);
|
||||||
|
byte[] key = deriveBytes.GetBytes(32); // 256 bit
|
||||||
|
byte[] iv = deriveBytes.GetBytes(16); // 128 bit
|
||||||
|
|
||||||
|
// AES 加密
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = key;
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
// 组合 Salt + 密文
|
||||||
|
byte[] result = new byte[salt.Length + cipherBytes.Length];
|
||||||
|
Buffer.BlockCopy(salt, 0, result, 0, salt.Length);
|
||||||
|
Buffer.BlockCopy(cipherBytes, 0, result, salt.Length, cipherBytes.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AES 解密(使用密码派生密钥)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherText">Base64 编码的密文</param>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <returns>明文</returns>
|
||||||
|
public static string AesDecrypt(string cipherText, string password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cipherText))
|
||||||
|
throw new ArgumentNullException(nameof(cipherText));
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
throw new ArgumentNullException(nameof(password));
|
||||||
|
|
||||||
|
byte[] fullCipher = Convert.FromBase64String(cipherText);
|
||||||
|
|
||||||
|
// 提取 Salt
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
Buffer.BlockCopy(fullCipher, 0, salt, 0, 16);
|
||||||
|
|
||||||
|
// 提取密文
|
||||||
|
byte[] cipherBytes = new byte[fullCipher.Length - 16];
|
||||||
|
Buffer.BlockCopy(fullCipher, 16, cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
// 从密码派生密钥
|
||||||
|
using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256);
|
||||||
|
byte[] key = deriveBytes.GetBytes(32);
|
||||||
|
byte[] iv = deriveBytes.GetBytes(16);
|
||||||
|
|
||||||
|
// AES 解密
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = key;
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(plainBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region RSA 加密解密
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 RSA 密钥对
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keySize">密钥大小(默认 2048 位)</param>
|
||||||
|
/// <returns>包含公钥和私钥的元组</returns>
|
||||||
|
public static (string PublicKey, string PrivateKey) GenerateRsaKeys(int keySize = 2048)
|
||||||
|
{
|
||||||
|
using var rsa = RSA.Create(keySize);
|
||||||
|
var publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||||
|
var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
|
||||||
|
return (publicKey, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA 公钥加密
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plainText">明文</param>
|
||||||
|
/// <param name="publicKey">Base64 编码的公钥</param>
|
||||||
|
/// <returns>Base64 编码的密文</returns>
|
||||||
|
public static string RsaEncrypt(string plainText, string publicKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plainText))
|
||||||
|
throw new ArgumentNullException(nameof(plainText));
|
||||||
|
if (string.IsNullOrEmpty(publicKey))
|
||||||
|
throw new ArgumentNullException(nameof(publicKey));
|
||||||
|
|
||||||
|
using var rsa = RSA.Create();
|
||||||
|
rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _);
|
||||||
|
|
||||||
|
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
byte[] cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(cipherBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSA 私钥解密
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherText">Base64 编码的密文</param>
|
||||||
|
/// <param name="privateKey">Base64 编码的私钥</param>
|
||||||
|
/// <returns>明文</returns>
|
||||||
|
public static string RsaDecrypt(string cipherText, string privateKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cipherText))
|
||||||
|
throw new ArgumentNullException(nameof(cipherText));
|
||||||
|
if (string.IsNullOrEmpty(privateKey))
|
||||||
|
throw new ArgumentNullException(nameof(privateKey));
|
||||||
|
|
||||||
|
using var rsa = RSA.Create();
|
||||||
|
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _);
|
||||||
|
|
||||||
|
byte[] cipherBytes = Convert.FromBase64String(cipherText);
|
||||||
|
byte[] plainBytes = rsa.Decrypt(cipherBytes, RSAEncryptionPadding.OaepSHA256);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(plainBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 哈希函数
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 MD5 哈希
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">输入字符串</param>
|
||||||
|
/// <returns>32 位十六进制哈希字符串</returns>
|
||||||
|
public static string Md5Hash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
byte[] hashBytes = md5.ComputeHash(inputBytes);
|
||||||
|
|
||||||
|
return BytesToHex(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 SHA256 哈希
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">输入字符串</param>
|
||||||
|
/// <returns>64 位十六进制哈希字符串</returns>
|
||||||
|
public static string Sha256Hash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
byte[] hashBytes = sha256.ComputeHash(inputBytes);
|
||||||
|
|
||||||
|
return BytesToHex(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 SHA512 哈希
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">输入字符串</param>
|
||||||
|
/// <returns>128 位十六进制哈希字符串</returns>
|
||||||
|
public static string Sha512Hash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
using var sha512 = SHA512.Create();
|
||||||
|
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
byte[] hashBytes = sha512.ComputeHash(inputBytes);
|
||||||
|
|
||||||
|
return BytesToHex(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BytesToHex(byte[] bytes)
|
||||||
|
{
|
||||||
|
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Base64 编解码
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 编码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">输入字符串</param>
|
||||||
|
/// <returns>Base64 编码的字符串</returns>
|
||||||
|
public static string Base64Encode(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 解码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Base64 编码的字符串</param>
|
||||||
|
/// <returns>解码后的字符串</returns>
|
||||||
|
public static string Base64Decode(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
byte[] bytes = Convert.FromBase64String(input);
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL 安全的 Base64 编码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">输入字符串</param>
|
||||||
|
/// <returns>URL 安全的 Base64 字符串</returns>
|
||||||
|
public static string Base64UrlEncode(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
var base64 = Base64Encode(input);
|
||||||
|
return base64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL 安全的 Base64 解码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">URL 安全的 Base64 字符串</param>
|
||||||
|
/// <returns>解码后的字符串</returns>
|
||||||
|
public static string Base64UrlDecode(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
|
var base64 = input.Replace('-', '+').Replace('_', '/');
|
||||||
|
switch (base64.Length % 4)
|
||||||
|
{
|
||||||
|
case 2: base64 += "=="; break;
|
||||||
|
case 3: base64 += "="; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base64Decode(base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 密码哈希(PBKDF2)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 PBKDF2 哈希密码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <param name="saltSize">Salt 大小(字节,默认 16)</param>
|
||||||
|
/// <param name="iterations">迭代次数(默认 100000)</param>
|
||||||
|
/// <param name="hashSize">哈希大小(字节,默认 32)</param>
|
||||||
|
/// <returns>格式化的哈希字符串(包含算法、迭代次数、Salt 和哈希值)</returns>
|
||||||
|
public static string HashPassword(string password, int saltSize = 16, int iterations = 100000, int hashSize = 32)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
throw new ArgumentNullException(nameof(password));
|
||||||
|
|
||||||
|
// 生成随机 Salt
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(saltSize);
|
||||||
|
|
||||||
|
// 使用 PBKDF2 计算哈希
|
||||||
|
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
|
||||||
|
byte[] hash = pbkdf2.GetBytes(hashSize);
|
||||||
|
|
||||||
|
// 格式: $pbkdf2-sha256$迭代次数$Salt$Hash
|
||||||
|
return $"$pbkdf2-sha256${iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证密码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">要验证的密码</param>
|
||||||
|
/// <param name="hashedPassword">存储的哈希密码</param>
|
||||||
|
/// <returns>密码是否匹配</returns>
|
||||||
|
public static bool VerifyPassword(string password, string hashedPassword)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
throw new ArgumentNullException(nameof(password));
|
||||||
|
if (string.IsNullOrEmpty(hashedPassword))
|
||||||
|
throw new ArgumentNullException(nameof(hashedPassword));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 解析哈希字符串
|
||||||
|
var parts = hashedPassword.Split('$');
|
||||||
|
if (parts.Length != 5 || parts[1] != "pbkdf2-sha256")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int iterations = int.Parse(parts[2]);
|
||||||
|
byte[] salt = Convert.FromBase64String(parts[3]);
|
||||||
|
byte[] storedHash = Convert.FromBase64String(parts[4]);
|
||||||
|
|
||||||
|
// 计算输入密码的哈希
|
||||||
|
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
|
||||||
|
byte[] computedHash = pbkdf2.GetBytes(storedHash.Length);
|
||||||
|
|
||||||
|
// 常量时间比较,防止时序攻击
|
||||||
|
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region HMAC 签名
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 HMAC-SHA256 计算签名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="secret">密钥</param>
|
||||||
|
/// <returns>十六进制签名字符串</returns>
|
||||||
|
public static string HmacSha256(string message, string secret)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
throw new ArgumentNullException(nameof(message));
|
||||||
|
if (string.IsNullOrEmpty(secret))
|
||||||
|
throw new ArgumentNullException(nameof(secret));
|
||||||
|
|
||||||
|
byte[] keyBytes = Encoding.UTF8.GetBytes(secret);
|
||||||
|
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(keyBytes);
|
||||||
|
byte[] hashBytes = hmac.ComputeHash(messageBytes);
|
||||||
|
|
||||||
|
return BytesToHex(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 HMAC-SHA256 签名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="signature">签名</param>
|
||||||
|
/// <param name="secret">密钥</param>
|
||||||
|
/// <returns>签名是否有效</returns>
|
||||||
|
public static bool VerifyHmacSha256(string message, string signature, string secret)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
throw new ArgumentNullException(nameof(message));
|
||||||
|
if (string.IsNullOrEmpty(signature))
|
||||||
|
throw new ArgumentNullException(nameof(signature));
|
||||||
|
if (string.IsNullOrEmpty(secret))
|
||||||
|
throw new ArgumentNullException(nameof(secret));
|
||||||
|
|
||||||
|
var computedSignature = HmacSha256(message, secret);
|
||||||
|
return string.Equals(computedSignature, signature, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 HMAC-SHA512 计算签名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="secret">密钥</param>
|
||||||
|
/// <returns>十六进制签名字符串</returns>
|
||||||
|
public static string HmacSha512(string message, string secret)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
throw new ArgumentNullException(nameof(message));
|
||||||
|
if (string.IsNullOrEmpty(secret))
|
||||||
|
throw new ArgumentNullException(nameof(secret));
|
||||||
|
|
||||||
|
byte[] keyBytes = Encoding.UTF8.GetBytes(secret);
|
||||||
|
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA512(keyBytes);
|
||||||
|
byte[] hashBytes = hmac.ComputeHash(messageBytes);
|
||||||
|
|
||||||
|
return BytesToHex(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 HMAC-SHA512 签名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="signature">签名</param>
|
||||||
|
/// <param name="secret">密钥</param>
|
||||||
|
/// <returns>签名是否有效</returns>
|
||||||
|
public static bool VerifyHmacSha512(string message, string signature, string secret)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
throw new ArgumentNullException(nameof(message));
|
||||||
|
if (string.IsNullOrEmpty(signature))
|
||||||
|
throw new ArgumentNullException(nameof(signature));
|
||||||
|
if (string.IsNullOrEmpty(secret))
|
||||||
|
throw new ArgumentNullException(nameof(secret));
|
||||||
|
|
||||||
|
var computedSignature = HmacSha512(message, secret);
|
||||||
|
return string.Equals(computedSignature, signature, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助方法
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成随机字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">长度</param>
|
||||||
|
/// <param name="useSpecialChars">是否包含特殊字符</param>
|
||||||
|
/// <returns>随机字符串</returns>
|
||||||
|
public static string GenerateRandomString(int length, bool useSpecialChars = false)
|
||||||
|
{
|
||||||
|
const string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const string numbers = "0123456789";
|
||||||
|
const string special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||||
|
|
||||||
|
var chars = letters + numbers;
|
||||||
|
if (useSpecialChars)
|
||||||
|
chars += special;
|
||||||
|
|
||||||
|
var result = new char[length];
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
result[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成随机密码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">密码长度(默认 16)</param>
|
||||||
|
/// <param name="requireDigit">是否必须包含数字</param>
|
||||||
|
/// <param name="requireLowercase">是否必须包含小写字母</param>
|
||||||
|
/// <param name="requireUppercase">是否必须包含大写字母</param>
|
||||||
|
/// <param name="requireSpecialChar">是否必须包含特殊字符</param>
|
||||||
|
/// <returns>随机密码</returns>
|
||||||
|
public static string GeneratePassword(
|
||||||
|
int length = 16,
|
||||||
|
bool requireDigit = true,
|
||||||
|
bool requireLowercase = true,
|
||||||
|
bool requireUppercase = true,
|
||||||
|
bool requireSpecialChar = true)
|
||||||
|
{
|
||||||
|
if (length < 4)
|
||||||
|
throw new ArgumentException("Password length must be at least 4", nameof(length));
|
||||||
|
|
||||||
|
const string lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const string uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const string digits = "0123456789";
|
||||||
|
const string special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||||
|
|
||||||
|
var chars = "";
|
||||||
|
var password = new List<char>();
|
||||||
|
|
||||||
|
// 确保满足要求
|
||||||
|
if (requireLowercase)
|
||||||
|
{
|
||||||
|
chars += lowercase;
|
||||||
|
password.Add(lowercase[RandomNumberGenerator.GetInt32(lowercase.Length)]);
|
||||||
|
}
|
||||||
|
if (requireUppercase)
|
||||||
|
{
|
||||||
|
chars += uppercase;
|
||||||
|
password.Add(uppercase[RandomNumberGenerator.GetInt32(uppercase.Length)]);
|
||||||
|
}
|
||||||
|
if (requireDigit)
|
||||||
|
{
|
||||||
|
chars += digits;
|
||||||
|
password.Add(digits[RandomNumberGenerator.GetInt32(digits.Length)]);
|
||||||
|
}
|
||||||
|
if (requireSpecialChar)
|
||||||
|
{
|
||||||
|
chars += special;
|
||||||
|
password.Add(special[RandomNumberGenerator.GetInt32(special.Length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充剩余长度
|
||||||
|
for (int i = password.Count; i < length; i++)
|
||||||
|
{
|
||||||
|
password.Add(chars[RandomNumberGenerator.GetInt32(chars.Length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机打乱
|
||||||
|
for (int i = password.Count - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
int j = RandomNumberGenerator.GetInt32(i + 1);
|
||||||
|
(password[i], password[j]) = (password[j], password[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(password.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举选项(用于下拉列表等场景)
|
||||||
|
/// </summary>
|
||||||
|
public class EnumOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举值(整数)
|
||||||
|
/// </summary>
|
||||||
|
public int Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举描述(来自 Description 特性)
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举显示文本(优先使用 Description,否则使用 Name)
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayText => string.IsNullOrEmpty(Description) ? Name : Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举工具类
|
||||||
|
/// 提供枚举解析、描述获取、标志位操作等功能
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumHelper
|
||||||
|
{
|
||||||
|
#region 枚举解析
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从字符串解析枚举值
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="value">字符串值</param>
|
||||||
|
/// <param name="ignoreCase">是否忽略大小写(默认 true)</param>
|
||||||
|
/// <returns>枚举值,解析失败返回 null</returns>
|
||||||
|
public static TEnum? Parse<TEnum>(string? value, bool ignoreCase = true) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (Enum.TryParse<TEnum>(value, ignoreCase, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从字符串解析枚举值(带默认值)
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum ParseOrDefault<TEnum>(string? value, TEnum defaultValue, bool ignoreCase = true)
|
||||||
|
where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Parse<TEnum>(value, ignoreCase) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从整数值解析枚举
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum? ParseFromInt<TEnum>(int value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
if (Enum.IsDefined(typeof(TEnum), value))
|
||||||
|
return (TEnum)(object)value;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从描述文本解析枚举值
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="description">描述文本</param>
|
||||||
|
/// <returns>枚举值,未找到返回 null</returns>
|
||||||
|
public static TEnum? ParseFromDescription<TEnum>(string? description) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(description))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
foreach (var field in typeof(TEnum).GetFields())
|
||||||
|
{
|
||||||
|
if (field.GetCustomAttribute<DescriptionAttribute>() is { } attribute)
|
||||||
|
{
|
||||||
|
if (attribute.Description.Equals(description, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return (TEnum)field.GetValue(null)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举描述
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的描述文本(来自 Description 特性)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">枚举值</param>
|
||||||
|
/// <returns>描述文本,如果没有 Description 特性则返回枚举名称</returns>
|
||||||
|
public static string GetDescription(this Enum value)
|
||||||
|
{
|
||||||
|
var field = value.GetType().GetField(value.ToString());
|
||||||
|
if (field == null)
|
||||||
|
return value.ToString();
|
||||||
|
|
||||||
|
if (field.GetCustomAttribute<DescriptionAttribute>() is { } attribute)
|
||||||
|
return attribute.Description;
|
||||||
|
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举类型的所有描述文本
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<TEnum, string> GetAllDescriptions<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>()
|
||||||
|
.ToDictionary(value => value, value => ((Enum)(object)value).GetDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举列表
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的所有值
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum[] GetValues<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的所有名称
|
||||||
|
/// </summary>
|
||||||
|
public static string[] GetNames<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetNames<TEnum>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举选项列表(用于下拉框等)
|
||||||
|
/// </summary>
|
||||||
|
public static List<EnumOption> GetOptions<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>()
|
||||||
|
.Select(value => new EnumOption
|
||||||
|
{
|
||||||
|
Value = Convert.ToInt32(value),
|
||||||
|
Name = value.ToString(),
|
||||||
|
Description = ((Enum)(object)value).GetDescription()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的键值对字典(值 -> 名称)
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<int, string> GetDictionary<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>()
|
||||||
|
.ToDictionary(value => Convert.ToInt32(value), value => value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的键值对字典(值 -> 描述)
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<int, string> GetDictionaryWithDescription<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>()
|
||||||
|
.ToDictionary(
|
||||||
|
value => Convert.ToInt32(value),
|
||||||
|
value => ((Enum)(object)value).GetDescription()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举值是否已定义
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDefined<TEnum>(int value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.IsDefined(typeof(TEnum), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举值是否已定义
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDefined<TEnum>(string name, bool ignoreCase = true) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.TryParse<TEnum>(name, ignoreCase, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举值是否已定义
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDefined<TEnum>(TEnum value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.IsDefined(typeof(TEnum), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 标志位枚举操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举是否包含指定标志位
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return ((Enum)(object)value).HasFlag((Enum)(object)flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加标志位
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum AddFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
int intValue = Convert.ToInt32(value);
|
||||||
|
int intFlag = Convert.ToInt32(flag);
|
||||||
|
return (TEnum)(object)(intValue | intFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除标志位
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum RemoveFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
int intValue = Convert.ToInt32(value);
|
||||||
|
int intFlag = Convert.ToInt32(flag);
|
||||||
|
return (TEnum)(object)(intValue & ~intFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换标志位(有则移除,无则添加)
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum ToggleFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
int intValue = Convert.ToInt32(value);
|
||||||
|
int intFlag = Convert.ToInt32(flag);
|
||||||
|
return (TEnum)(object)(intValue ^ intFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举中设置的所有标志位
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TEnum> GetFlags<TEnum>(this TEnum value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
foreach (TEnum flag in Enum.GetValues<TEnum>())
|
||||||
|
{
|
||||||
|
if (value.HasFlag(flag))
|
||||||
|
yield return flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从多个标志位组合成枚举值
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum CombineFlags<TEnum>(params TEnum[] flags) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
foreach (var flag in flags)
|
||||||
|
{
|
||||||
|
result |= Convert.ToInt32(flag);
|
||||||
|
}
|
||||||
|
return (TEnum)(object)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举转换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举转整数
|
||||||
|
/// </summary>
|
||||||
|
public static int ToInt<TEnum>(this TEnum value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Convert.ToInt32(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举转长整数
|
||||||
|
/// </summary>
|
||||||
|
public static long ToLong<TEnum>(this TEnum value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 整数转枚举
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum ToEnum<TEnum>(this int value) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return (TEnum)(object)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举信息
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的数量
|
||||||
|
/// </summary>
|
||||||
|
public static int GetCount<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>().Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的最小值
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum GetMinValue<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>().MinBy(v => Convert.ToInt32(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的最大值
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum GetMaxValue<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues<TEnum>().MaxBy(v => Convert.ToInt32(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举类型是否为标志位枚举(有 Flags 特性)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFlagsEnum<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return typeof(TEnum).GetCustomAttribute<FlagsAttribute>() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举比较
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 比较两个枚举值是否相等
|
||||||
|
/// </summary>
|
||||||
|
public static bool Equals<TEnum>(TEnum value1, TEnum value2) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return EqualityComparer<TEnum>.Default.Equals(value1, value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举值是否在指定列表中
|
||||||
|
/// </summary>
|
||||||
|
public static bool In<TEnum>(this TEnum value, params TEnum[] values) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return values.Contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断枚举值是否不在指定列表中
|
||||||
|
/// </summary>
|
||||||
|
public static bool NotIn<TEnum>(this TEnum value, params TEnum[] values) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
return !values.Contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 随机枚举
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取随机枚举值
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum GetRandom<TEnum>() where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
var values = Enum.GetValues<TEnum>();
|
||||||
|
var random = new Random();
|
||||||
|
return values[random.Next(values.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取随机枚举值(排除指定值)
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum GetRandomExcept<TEnum>(params TEnum[] except) where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
var values = Enum.GetValues<TEnum>().Except(except).ToArray();
|
||||||
|
if (values.Length == 0)
|
||||||
|
throw new InvalidOperationException("No available enum values after exclusion");
|
||||||
|
|
||||||
|
var random = new Random();
|
||||||
|
return values[random.Next(values.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,873 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件操作工具类
|
||||||
|
/// 提供文件读写、复制、移动、删除、搜索、大小格式化、哈希计算等功能
|
||||||
|
/// </summary>
|
||||||
|
public static class FileHelper
|
||||||
|
{
|
||||||
|
#region 文件读取
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取文件所有文本内容
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">文件路径</param>
|
||||||
|
/// <param name="encoding">编码方式(默认 UTF-8)</param>
|
||||||
|
/// <returns>文件内容</returns>
|
||||||
|
public static string ReadAllText(string filePath, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.ReadAllText(filePath, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取文件所有行
|
||||||
|
/// </summary>
|
||||||
|
public static string[] ReadAllLines(string filePath, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.ReadAllLines(filePath, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取文件所有字节
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] ReadAllBytes(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.ReadAllBytes(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 逐行读取大文件(不会一次性加载到内存)
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<string> ReadLines(string filePath, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.ReadLines(filePath, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件写入
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入文本到文件(覆盖原内容)
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteAllText(string filePath, string content, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
EnsureDirectoryExists(filePath);
|
||||||
|
File.WriteAllText(filePath, content, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入文本到文件(追加内容)
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendAllText(string filePath, string content, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
EnsureDirectoryExists(filePath);
|
||||||
|
File.AppendAllText(filePath, content, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入行到文件(覆盖原内容)
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteAllLines(string filePath, IEnumerable<string> lines, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
EnsureDirectoryExists(filePath);
|
||||||
|
File.WriteAllLines(filePath, lines, encoding ?? Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入字节到文件(覆盖原内容)
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteAllBytes(string filePath, byte[] bytes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
EnsureDirectoryExists(filePath);
|
||||||
|
File.WriteAllBytes(filePath, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceFilePath">源文件路径</param>
|
||||||
|
/// <param name="destFilePath">目标文件路径</param>
|
||||||
|
/// <param name="overwrite">是否覆盖已存在的文件(默认 true)</param>
|
||||||
|
public static void CopyFile(string sourceFilePath, string destFilePath, bool overwrite = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceFilePath))
|
||||||
|
throw new ArgumentNullException(nameof(sourceFilePath));
|
||||||
|
if (string.IsNullOrWhiteSpace(destFilePath))
|
||||||
|
throw new ArgumentNullException(nameof(destFilePath));
|
||||||
|
|
||||||
|
if (!File.Exists(sourceFilePath))
|
||||||
|
throw new FileNotFoundException($"Source file not found: {sourceFilePath}");
|
||||||
|
|
||||||
|
EnsureDirectoryExists(destFilePath);
|
||||||
|
File.Copy(sourceFilePath, destFilePath, overwrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移动文件
|
||||||
|
/// </summary>
|
||||||
|
public static void MoveFile(string sourceFilePath, string destFilePath, bool overwrite = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceFilePath))
|
||||||
|
throw new ArgumentNullException(nameof(sourceFilePath));
|
||||||
|
if (string.IsNullOrWhiteSpace(destFilePath))
|
||||||
|
throw new ArgumentNullException(nameof(destFilePath));
|
||||||
|
|
||||||
|
if (!File.Exists(sourceFilePath))
|
||||||
|
throw new FileNotFoundException($"Source file not found: {sourceFilePath}");
|
||||||
|
|
||||||
|
EnsureDirectoryExists(destFilePath);
|
||||||
|
File.Move(sourceFilePath, destFilePath, overwrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除文件(如果存在)
|
||||||
|
/// </summary>
|
||||||
|
public static void DeleteFile(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
File.Delete(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断文件是否存在
|
||||||
|
/// </summary>
|
||||||
|
public static bool FileExists(string filePath)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重命名文件
|
||||||
|
/// </summary>
|
||||||
|
public static void RenameFile(string filePath, string newFileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
if (string.IsNullOrWhiteSpace(newFileName))
|
||||||
|
throw new ArgumentNullException(nameof(newFileName));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
var newFilePath = Path.Combine(directory ?? "", newFileName);
|
||||||
|
File.Move(filePath, newFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 目录操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建目录(如果不存在)
|
||||||
|
/// </summary>
|
||||||
|
public static void CreateDirectory(string directoryPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除目录(包括子目录和文件)
|
||||||
|
/// </summary>
|
||||||
|
public static void DeleteDirectory(string directoryPath, bool recursive = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (Directory.Exists(directoryPath))
|
||||||
|
Directory.Delete(directoryPath, recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断目录是否存在
|
||||||
|
/// </summary>
|
||||||
|
public static bool DirectoryExists(string directoryPath)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(directoryPath) && Directory.Exists(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确保文件所在目录存在(不存在则创建)
|
||||||
|
/// </summary>
|
||||||
|
public static void EnsureDirectoryExists(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制目录(包括所有子目录和文件)
|
||||||
|
/// </summary>
|
||||||
|
public static void CopyDirectory(string sourceDir, string destDir, bool recursive = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceDir))
|
||||||
|
throw new ArgumentNullException(nameof(sourceDir));
|
||||||
|
if (string.IsNullOrWhiteSpace(destDir))
|
||||||
|
throw new ArgumentNullException(nameof(destDir));
|
||||||
|
|
||||||
|
var dir = new DirectoryInfo(sourceDir);
|
||||||
|
if (!dir.Exists)
|
||||||
|
throw new DirectoryNotFoundException($"Source directory not found: {sourceDir}");
|
||||||
|
|
||||||
|
// 创建目标目录
|
||||||
|
Directory.CreateDirectory(destDir);
|
||||||
|
|
||||||
|
// 复制文件
|
||||||
|
foreach (var file in dir.GetFiles())
|
||||||
|
{
|
||||||
|
var targetFilePath = Path.Combine(destDir, file.Name);
|
||||||
|
file.CopyTo(targetFilePath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归复制子目录
|
||||||
|
if (recursive)
|
||||||
|
{
|
||||||
|
foreach (var subDir in dir.GetDirectories())
|
||||||
|
{
|
||||||
|
var targetDirPath = Path.Combine(destDir, subDir.Name);
|
||||||
|
CopyDirectory(subDir.FullName, targetDirPath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移动目录
|
||||||
|
/// </summary>
|
||||||
|
public static void MoveDirectory(string sourceDir, string destDir)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceDir))
|
||||||
|
throw new ArgumentNullException(nameof(sourceDir));
|
||||||
|
if (string.IsNullOrWhiteSpace(destDir))
|
||||||
|
throw new ArgumentNullException(nameof(destDir));
|
||||||
|
|
||||||
|
if (!Directory.Exists(sourceDir))
|
||||||
|
throw new DirectoryNotFoundException($"Source directory not found: {sourceDir}");
|
||||||
|
|
||||||
|
Directory.Move(sourceDir, destDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件搜索
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录下的所有文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directoryPath">目录路径</param>
|
||||||
|
/// <param name="searchPattern">搜索模式(例如 "*.txt")</param>
|
||||||
|
/// <param name="recursive">是否搜索子目录(默认 false)</param>
|
||||||
|
/// <returns>文件路径列表</returns>
|
||||||
|
public static string[] GetFiles(string directoryPath, string searchPattern = "*", bool recursive = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
return Directory.GetFiles(directoryPath, searchPattern, searchOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录下的所有子目录
|
||||||
|
/// </summary>
|
||||||
|
public static string[] GetDirectories(string directoryPath, string searchPattern = "*", bool recursive = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
return Directory.GetDirectories(directoryPath, searchPattern, searchOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索文件(支持多个模式)
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<string> SearchFiles(string directoryPath, string[] searchPatterns, bool recursive = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
var files = new List<string>();
|
||||||
|
foreach (var pattern in searchPatterns)
|
||||||
|
{
|
||||||
|
files.AddRange(GetFiles(directoryPath, pattern, recursive));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件信息
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件大小(字节)
|
||||||
|
/// </summary>
|
||||||
|
public static long GetFileSize(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return new FileInfo(filePath).Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化文件大小(自动选择单位:B, KB, MB, GB, TB)
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatFileSize(long bytes, int decimalPlaces = 2)
|
||||||
|
{
|
||||||
|
string[] units = { "B", "KB", "MB", "GB", "TB", "PB" };
|
||||||
|
|
||||||
|
if (bytes == 0)
|
||||||
|
return $"0 {units[0]}";
|
||||||
|
|
||||||
|
var unitIndex = 0;
|
||||||
|
double size = bytes;
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.Length - 1)
|
||||||
|
{
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{Math.Round(size, decimalPlaces)} {units[unitIndex]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件扩展名(包含点号,例如 ".txt")
|
||||||
|
/// </summary>
|
||||||
|
public static string GetExtension(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Path.GetExtension(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件名(不含扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFileNameWithoutExtension(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件名(含扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFileName(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Path.GetFileName(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件所在目录路径
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetDirectoryName(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Path.GetDirectoryName(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件创建时间
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime GetCreationTime(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.GetCreationTime(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件最后修改时间
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime GetLastWriteTime(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.GetLastWriteTime(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取文件最后访问时间
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime GetLastAccessTime(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
return File.GetLastAccessTime(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件哈希
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算文件的 MD5 哈希
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFileMd5(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
var hash = md5.ComputeHash(stream);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算文件的 SHA256 哈希
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFileSha256(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
var hash = sha256.ComputeHash(stream);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算文件的 SHA512 哈希
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFileSha512(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
using var sha512 = SHA512.Create();
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
var hash = sha512.ComputeHash(stream);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 比较两个文件是否相同(通过 SHA256 哈希)
|
||||||
|
/// </summary>
|
||||||
|
public static bool AreFilesEqual(string filePath1, string filePath2)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath1) || !File.Exists(filePath2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 先比较大小
|
||||||
|
if (GetFileSize(filePath1) != GetFileSize(filePath2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 再比较哈希
|
||||||
|
return GetFileSha256(filePath1) == GetFileSha256(filePath2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 路径操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组合路径
|
||||||
|
/// </summary>
|
||||||
|
public static string CombinePaths(params string[] paths)
|
||||||
|
{
|
||||||
|
if (paths == null || paths.Length == 0)
|
||||||
|
throw new ArgumentException("At least one path is required", nameof(paths));
|
||||||
|
|
||||||
|
return Path.Combine(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规范化路径(转换为绝对路径,统一分隔符)
|
||||||
|
/// </summary>
|
||||||
|
public static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return Path.GetFullPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取相对路径
|
||||||
|
/// </summary>
|
||||||
|
public static string GetRelativePath(string basePath, string targetPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(basePath))
|
||||||
|
throw new ArgumentNullException(nameof(basePath));
|
||||||
|
if (string.IsNullOrWhiteSpace(targetPath))
|
||||||
|
throw new ArgumentNullException(nameof(targetPath));
|
||||||
|
|
||||||
|
return Path.GetRelativePath(basePath, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断路径是否为绝对路径
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsAbsolutePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Path.IsPathFullyQualified(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成唯一文件名(如果文件已存在,添加数字后缀)
|
||||||
|
/// </summary>
|
||||||
|
public static string GetUniqueFilePath(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return filePath;
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath) ?? "";
|
||||||
|
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
var extension = Path.GetExtension(filePath);
|
||||||
|
|
||||||
|
var counter = 1;
|
||||||
|
string newFilePath;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var newFileName = $"{fileNameWithoutExt} ({counter}){extension}";
|
||||||
|
newFilePath = Path.Combine(directory, newFileName);
|
||||||
|
counter++;
|
||||||
|
} while (File.Exists(newFilePath));
|
||||||
|
|
||||||
|
return newFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取临时文件路径
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTempFilePath(string? extension = null)
|
||||||
|
{
|
||||||
|
var tempPath = Path.GetTempFileName();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(extension))
|
||||||
|
{
|
||||||
|
if (!extension.StartsWith('.'))
|
||||||
|
extension = "." + extension;
|
||||||
|
|
||||||
|
var newPath = Path.ChangeExtension(tempPath, extension);
|
||||||
|
File.Move(tempPath, newPath);
|
||||||
|
return newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件类型判断
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否为文本文件(根据扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsTextFile(string filePath)
|
||||||
|
{
|
||||||
|
var extension = GetExtension(filePath).ToLowerInvariant();
|
||||||
|
string[] textExtensions = { ".txt", ".cs", ".xml", ".json", ".html", ".css", ".js", ".ts", ".md", ".log", ".csv", ".sql" };
|
||||||
|
return textExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否为图片文件(根据扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsImageFile(string filePath)
|
||||||
|
{
|
||||||
|
var extension = GetExtension(filePath).ToLowerInvariant();
|
||||||
|
string[] imageExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico" };
|
||||||
|
return imageExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否为视频文件(根据扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsVideoFile(string filePath)
|
||||||
|
{
|
||||||
|
var extension = GetExtension(filePath).ToLowerInvariant();
|
||||||
|
string[] videoExtensions = { ".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v" };
|
||||||
|
return videoExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否为音频文件(根据扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsAudioFile(string filePath)
|
||||||
|
{
|
||||||
|
var extension = GetExtension(filePath).ToLowerInvariant();
|
||||||
|
string[] audioExtensions = { ".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a" };
|
||||||
|
return audioExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否为压缩文件(根据扩展名)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsArchiveFile(string filePath)
|
||||||
|
{
|
||||||
|
var extension = GetExtension(filePath).ToLowerInvariant();
|
||||||
|
string[] archiveExtensions = { ".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz" };
|
||||||
|
return archiveExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件监控
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建文件监视器
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directoryPath">监视的目录</param>
|
||||||
|
/// <param name="filter">文件过滤器(例如 "*.txt")</param>
|
||||||
|
/// <param name="includeSubdirectories">是否监视子目录</param>
|
||||||
|
/// <returns>文件监视器</returns>
|
||||||
|
public static FileSystemWatcher CreateFileWatcher(
|
||||||
|
string directoryPath,
|
||||||
|
string filter = "*.*",
|
||||||
|
bool includeSubdirectories = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
throw new DirectoryNotFoundException($"Directory not found: {directoryPath}");
|
||||||
|
|
||||||
|
var watcher = new FileSystemWatcher(directoryPath)
|
||||||
|
{
|
||||||
|
Filter = filter,
|
||||||
|
IncludeSubdirectories = includeSubdirectories,
|
||||||
|
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite
|
||||||
|
};
|
||||||
|
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件清理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除目录中的所有文件(保留目录结构)
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearDirectory(string directoryPath, string searchPattern = "*", bool recursive = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var files = GetFiles(directoryPath, searchPattern, recursive);
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
DeleteFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除超过指定天数的文件
|
||||||
|
/// </summary>
|
||||||
|
public static int DeleteOldFiles(string directoryPath, int daysOld, string searchPattern = "*", bool recursive = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
throw new ArgumentNullException(nameof(directoryPath));
|
||||||
|
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var cutoffDate = DateTime.Now.AddDays(-daysOld);
|
||||||
|
var files = GetFiles(directoryPath, searchPattern, recursive);
|
||||||
|
var deletedCount = 0;
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (GetLastWriteTime(file) < cutoffDate)
|
||||||
|
{
|
||||||
|
DeleteFile(file);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空回收站中的文件(Windows)
|
||||||
|
/// </summary>
|
||||||
|
public static void EmptyRecycleBin()
|
||||||
|
{
|
||||||
|
// 注意:这个功能需要 Windows API,在跨平台应用中需要特殊处理
|
||||||
|
// 这里仅提供接口,实际实现需要根据平台调整
|
||||||
|
throw new NotImplementedException("This feature requires platform-specific implementation");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件属性
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置文件为只读
|
||||||
|
/// </summary>
|
||||||
|
public static void SetReadOnly(string filePath, bool isReadOnly = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
if (isReadOnly)
|
||||||
|
fileInfo.Attributes |= FileAttributes.ReadOnly;
|
||||||
|
else
|
||||||
|
fileInfo.Attributes &= ~FileAttributes.ReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断文件是否为只读
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsReadOnly(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
return (fileInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置文件为隐藏
|
||||||
|
/// </summary>
|
||||||
|
public static void SetHidden(string filePath, bool isHidden = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
if (isHidden)
|
||||||
|
fileInfo.Attributes |= FileAttributes.Hidden;
|
||||||
|
else
|
||||||
|
fileInfo.Attributes &= ~FileAttributes.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断文件是否为隐藏
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsHidden(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
return (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,26 +1,398 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Common.Utils;
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 序列化工具类
|
||||||
|
/// 基于 System.Text.Json 提供 JSON 序列化、反序列化、格式化等功能
|
||||||
|
/// </summary>
|
||||||
public static class JsonHelper
|
public static class JsonHelper
|
||||||
{
|
{
|
||||||
|
#region 预定义选项
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 驼峰命名选项(默认格式化输出)
|
||||||
|
/// </summary>
|
||||||
public static readonly JsonSerializerOptions CamelCaseOptions = new JsonSerializerOptions
|
public static readonly JsonSerializerOptions CamelCaseOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
Converters =
|
Converters =
|
||||||
{
|
{
|
||||||
new IpAddressConverter(), new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false)
|
new IpAddressConverter(),
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ToJson<T>(T value) =>
|
/// <summary>
|
||||||
JsonSerializer.Serialize(value, CamelCaseOptions);
|
/// 驼峰命名选项(紧凑格式,不格式化输出)
|
||||||
|
/// </summary>
|
||||||
|
public static readonly JsonSerializerOptions CamelCaseCompactOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new IpAddressConverter(),
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public static T? FromJson<T>(string json) =>
|
/// <summary>
|
||||||
JsonSerializer.Deserialize<T>(json, CamelCaseOptions);
|
/// 蛇形命名选项(snake_case)
|
||||||
|
/// </summary>
|
||||||
|
public static readonly JsonSerializerOptions SnakeCaseOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new IpAddressConverter(),
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, allowIntegerValues: false)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认选项(保留原始命名,格式化输出)
|
||||||
|
/// </summary>
|
||||||
|
public static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new IpAddressConverter(),
|
||||||
|
new JsonStringEnumConverter(allowIntegerValues: false)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 基本序列化/反序列化
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象为 JSON 字符串(使用驼峰命名)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToJson<T>(T value, bool useCompact = false)
|
||||||
|
{
|
||||||
|
var options = useCompact ? CamelCaseCompactOptions : CamelCaseOptions;
|
||||||
|
return JsonSerializer.Serialize(value, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象为 JSON 字符串(自定义选项)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToJson<T>(T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(value, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 JSON 字符串反序列化对象(使用驼峰命名)
|
||||||
|
/// </summary>
|
||||||
|
public static T? FromJson<T>(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(json, CamelCaseOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 JSON 字符串反序列化对象(自定义选项)
|
||||||
|
/// </summary>
|
||||||
|
public static T? FromJson<T>(string json, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(json, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从 JSON 字符串反序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryFromJson<T>(string? json, out T? result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = JsonSerializer.Deserialize<T>(json, CamelCaseOptions);
|
||||||
|
return result != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 文件操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象到文件
|
||||||
|
/// </summary>
|
||||||
|
public static void ToJsonFile<T>(string filePath, T value, bool useCompact = false)
|
||||||
|
{
|
||||||
|
var json = ToJson(value, useCompact);
|
||||||
|
File.WriteAllText(filePath, json, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从文件反序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static T? FromJsonFile<T>(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var json = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
return FromJson<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从文件反序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryFromJsonFile<T>(string filePath, out T? result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
return TryFromJson(json, out result);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Stream 操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象到流
|
||||||
|
/// </summary>
|
||||||
|
public static async Task SerializeToStreamAsync<T>(Stream stream, T value, bool useCompact = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var options = useCompact ? CamelCaseCompactOptions : CamelCaseOptions;
|
||||||
|
await JsonSerializer.SerializeAsync(stream, value, options, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从流反序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T?> DeserializeFromStreamAsync<T>(Stream stream, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await JsonSerializer.DeserializeAsync<T>(stream, CamelCaseOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region JSON 格式化/压缩
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化 JSON 字符串(美化输出)
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatJson(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 压缩 JSON 字符串(移除空格和换行)
|
||||||
|
/// </summary>
|
||||||
|
public static string CompactJson(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = false });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region JSON 验证和查询
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 JSON 字符串格式是否有效
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsValidJson(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 JSON 字符串中提取指定路径的值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="json">JSON 字符串</param>
|
||||||
|
/// <param name="path">路径(例如 "user.name")</param>
|
||||||
|
/// <returns>提取的值(字符串形式)</returns>
|
||||||
|
public static string? GetValueByPath(string json, string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json) || string.IsNullOrWhiteSpace(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var element = doc.RootElement;
|
||||||
|
|
||||||
|
foreach (var key in path.Split('.'))
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(key, out var property))
|
||||||
|
{
|
||||||
|
element = property;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.ToString();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 深拷贝
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 JSON 序列化实现对象深拷贝
|
||||||
|
/// </summary>
|
||||||
|
public static T? DeepClone<T>(T obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var json = ToJson(obj);
|
||||||
|
return FromJson<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 合并 JSON
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合并两个 JSON 对象(后者覆盖前者)
|
||||||
|
/// </summary>
|
||||||
|
public static string MergeJson(string json1, string json2)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json1))
|
||||||
|
return json2 ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json2))
|
||||||
|
return json1;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc1 = JsonDocument.Parse(json1);
|
||||||
|
using var doc2 = JsonDocument.Parse(json2);
|
||||||
|
|
||||||
|
var merged = new Dictionary<string, JsonElement>();
|
||||||
|
|
||||||
|
// 添加第一个 JSON 的所有属性
|
||||||
|
foreach (var property in doc1.RootElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
merged[property.Name] = property.Value.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用第二个 JSON 的属性覆盖
|
||||||
|
foreach (var property in doc2.RootElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
merged[property.Name] = property.Value.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(merged, CamelCaseOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return json1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 类型转换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将对象转换为字典(通过 JSON 序列化/反序列化)
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, object?>? ToDictionary(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var json = ToJson(obj);
|
||||||
|
return FromJson<Dictionary<string, object?>>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从字典创建对象(通过 JSON 序列化/反序列化)
|
||||||
|
/// </summary>
|
||||||
|
public static T? FromDictionary<T>(Dictionary<string, object?> dict)
|
||||||
|
{
|
||||||
|
if (dict == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var json = ToJson(dict);
|
||||||
|
return FromJson<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,525 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 属性访问器(用于缓存,提高反射性能)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">类型</typeparam>
|
||||||
|
internal class PropertyAccessor<T>
|
||||||
|
{
|
||||||
|
public Func<T, object?>? Getter { get; set; }
|
||||||
|
public Action<T, object?>? Setter { get; set; }
|
||||||
|
public PropertyInfo PropertyInfo { get; set; }
|
||||||
|
|
||||||
|
public PropertyAccessor(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
PropertyInfo = propertyInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反射工具类
|
||||||
|
/// 提供类型扫描、属性访问、特性读取等功能,并通过缓存提高性能
|
||||||
|
/// </summary>
|
||||||
|
public static class ReflectionHelper
|
||||||
|
{
|
||||||
|
// 属性访问器缓存
|
||||||
|
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> PropertyAccessorCache = new();
|
||||||
|
|
||||||
|
// 特性缓存
|
||||||
|
private static readonly ConcurrentDictionary<MemberInfo, object[]> AttributeCache = new();
|
||||||
|
|
||||||
|
// 类型缓描
|
||||||
|
private static readonly ConcurrentDictionary<Assembly, Type[]> TypeCache = new();
|
||||||
|
|
||||||
|
#region 类型扫描
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从程序集中获取所有类型
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">程序集</param>
|
||||||
|
/// <param name="useCache">是否使用缓存(默认 true)</param>
|
||||||
|
/// <returns>类型数组</returns>
|
||||||
|
public static Type[] GetTypes(Assembly assembly, bool useCache = true)
|
||||||
|
{
|
||||||
|
if (useCache && TypeCache.TryGetValue(assembly, out var cachedTypes))
|
||||||
|
return cachedTypes;
|
||||||
|
|
||||||
|
var types = assembly.GetTypes();
|
||||||
|
|
||||||
|
if (useCache)
|
||||||
|
TypeCache.TryAdd(assembly, types);
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从当前程序集中查找指定基类的所有子类
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TBase">基类类型</typeparam>
|
||||||
|
/// <param name="includeAbstract">是否包含抽象类(默认 false)</param>
|
||||||
|
/// <returns>子类类型列表</returns>
|
||||||
|
public static List<Type> FindSubclassesOf<TBase>(bool includeAbstract = false)
|
||||||
|
{
|
||||||
|
return FindSubclassesOf(typeof(TBase), includeAbstract);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从当前程序集中查找指定基类的所有子类
|
||||||
|
/// </summary>
|
||||||
|
public static List<Type> FindSubclassesOf(Type baseType, bool includeAbstract = false)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetCallingAssembly();
|
||||||
|
return GetTypes(assembly)
|
||||||
|
.Where(t => t.IsClass && baseType.IsAssignableFrom(t) && t != baseType)
|
||||||
|
.Where(t => includeAbstract || !t.IsAbstract)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从指定程序集中查找实现指定接口的所有类型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInterface">接口类型</typeparam>
|
||||||
|
/// <param name="assembly">程序集</param>
|
||||||
|
/// <param name="includeAbstract">是否包含抽象类(默认 false)</param>
|
||||||
|
/// <returns>实现类型列表</returns>
|
||||||
|
public static List<Type> FindImplementationsOf<TInterface>(Assembly assembly, bool includeAbstract = false)
|
||||||
|
{
|
||||||
|
var interfaceType = typeof(TInterface);
|
||||||
|
return GetTypes(assembly)
|
||||||
|
.Where(t => t.IsClass && interfaceType.IsAssignableFrom(t) && t != interfaceType)
|
||||||
|
.Where(t => includeAbstract || !t.IsAbstract)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找带有指定特性的所有类型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TAttribute">特性类型</typeparam>
|
||||||
|
/// <param name="assembly">程序集</param>
|
||||||
|
/// <param name="inherit">是否搜索继承链(默认 true)</param>
|
||||||
|
/// <returns>类型列表</returns>
|
||||||
|
public static List<Type> FindTypesWithAttribute<TAttribute>(Assembly assembly, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
return GetTypes(assembly)
|
||||||
|
.Where(t => t.GetCustomAttribute<TAttribute>(inherit) != null)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 属性访问(高性能)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取属性值(使用缓存的访问器,比直接反射快)
|
||||||
|
/// </summary>
|
||||||
|
public static object? GetPropertyValue<T>(T obj, string propertyName)
|
||||||
|
{
|
||||||
|
var accessor = GetPropertyAccessor<T>(propertyName);
|
||||||
|
return accessor?.Getter?.Invoke(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置属性值(使用缓存的访问器)
|
||||||
|
/// </summary>
|
||||||
|
public static void SetPropertyValue<T>(T obj, string propertyName, object? value)
|
||||||
|
{
|
||||||
|
var accessor = GetPropertyAccessor<T>(propertyName);
|
||||||
|
accessor?.Setter?.Invoke(obj, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取属性访问器(内部使用表达式树生成,首次慢,后续快)
|
||||||
|
/// </summary>
|
||||||
|
private static PropertyAccessor<T>? GetPropertyAccessor<T>(string propertyName)
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
var cache = PropertyAccessorCache.GetOrAdd(type, _ => new Dictionary<string, object>());
|
||||||
|
|
||||||
|
lock (cache)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(propertyName, out var cachedAccessor))
|
||||||
|
return (PropertyAccessor<T>)cachedAccessor;
|
||||||
|
|
||||||
|
var propertyInfo = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
if (propertyInfo == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var accessor = new PropertyAccessor<T>(propertyInfo);
|
||||||
|
|
||||||
|
// 使用表达式树生成 Getter
|
||||||
|
if (propertyInfo.CanRead)
|
||||||
|
{
|
||||||
|
var instance = Expression.Parameter(type, "instance");
|
||||||
|
var property = Expression.Property(instance, propertyInfo);
|
||||||
|
var convert = Expression.Convert(property, typeof(object));
|
||||||
|
accessor.Getter = Expression.Lambda<Func<T, object?>>(convert, instance).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用表达式树生成 Setter
|
||||||
|
if (propertyInfo.CanWrite)
|
||||||
|
{
|
||||||
|
var instance = Expression.Parameter(type, "instance");
|
||||||
|
var value = Expression.Parameter(typeof(object), "value");
|
||||||
|
var convert = Expression.Convert(value, propertyInfo.PropertyType);
|
||||||
|
var property = Expression.Property(instance, propertyInfo);
|
||||||
|
var assign = Expression.Assign(property, convert);
|
||||||
|
accessor.Setter = Expression.Lambda<Action<T, object?>>(assign, instance, value).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[propertyName] = accessor;
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有属性名称
|
||||||
|
/// </summary>
|
||||||
|
public static List<string> GetPropertyNames<T>(BindingFlags flags = BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
{
|
||||||
|
return typeof(T).GetProperties(flags).Select(p => p.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有属性信息
|
||||||
|
/// </summary>
|
||||||
|
public static PropertyInfo[] GetProperties<T>(BindingFlags flags = BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
{
|
||||||
|
return typeof(T).GetProperties(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 特性读取(带缓存)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取类型的特性(带缓存)
|
||||||
|
/// </summary>
|
||||||
|
public static TAttribute? GetAttribute<TAttribute>(Type type, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
var attributes = GetAttributes<TAttribute>(type, inherit);
|
||||||
|
return attributes.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取类型的所有指定特性(带缓存)
|
||||||
|
/// </summary>
|
||||||
|
public static TAttribute[] GetAttributes<TAttribute>(Type type, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
var cached = AttributeCache.GetOrAdd(type, _ => type.GetCustomAttributes(inherit).ToArray());
|
||||||
|
return cached.OfType<TAttribute>().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取成员的特性(带缓存)
|
||||||
|
/// </summary>
|
||||||
|
public static TAttribute? GetAttribute<TAttribute>(MemberInfo member, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
var attributes = GetAttributes<TAttribute>(member, inherit);
|
||||||
|
return attributes.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取成员的所有指定特性(带缓存)
|
||||||
|
/// </summary>
|
||||||
|
public static TAttribute[] GetAttributes<TAttribute>(MemberInfo member, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
var cached = AttributeCache.GetOrAdd(member, _ => member.GetCustomAttributes(inherit).ToArray());
|
||||||
|
return cached.OfType<TAttribute>().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否有指定特性
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasAttribute<TAttribute>(Type type, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
return GetAttribute<TAttribute>(type, inherit) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断成员是否有指定特性
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasAttribute<TAttribute>(MemberInfo member, bool inherit = true)
|
||||||
|
where TAttribute : Attribute
|
||||||
|
{
|
||||||
|
return GetAttribute<TAttribute>(member, inherit) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 创建实例
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建类型实例(使用无参构造函数)
|
||||||
|
/// </summary>
|
||||||
|
public static T? CreateInstance<T>() where T : class
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建类型实例(使用无参构造函数)
|
||||||
|
/// </summary>
|
||||||
|
public static object? CreateInstance(Type type)
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建类型实例(使用有参构造函数)
|
||||||
|
/// </summary>
|
||||||
|
public static object? CreateInstance(Type type, params object[] args)
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance(type, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试创建类型实例
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryCreateInstance<T>(out T? instance) where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
instance = CreateInstance<T>();
|
||||||
|
return instance != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
instance = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 属性复制
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将源对象的属性值复制到目标对象(同名属性)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TSource">源类型</typeparam>
|
||||||
|
/// <typeparam name="TTarget">目标类型</typeparam>
|
||||||
|
/// <param name="source">源对象</param>
|
||||||
|
/// <param name="target">目标对象</param>
|
||||||
|
/// <param name="ignoreCase">是否忽略属性名大小写(默认 false)</param>
|
||||||
|
public static void CopyProperties<TSource, TTarget>(TSource source, TTarget target, bool ignoreCase = false)
|
||||||
|
{
|
||||||
|
if (source == null || target == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sourceType = typeof(TSource);
|
||||||
|
var targetType = typeof(TTarget);
|
||||||
|
|
||||||
|
var sourceProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var targetProperties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanWrite)
|
||||||
|
.ToDictionary(
|
||||||
|
p => p.Name,
|
||||||
|
p => p,
|
||||||
|
ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var sourceProp in sourceProperties)
|
||||||
|
{
|
||||||
|
if (targetProperties.TryGetValue(sourceProp.Name, out var targetProp))
|
||||||
|
{
|
||||||
|
// 检查类型是否兼容
|
||||||
|
if (targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
|
||||||
|
{
|
||||||
|
var value = sourceProp.GetValue(source);
|
||||||
|
targetProp.SetValue(target, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建目标类型实例并复制属性
|
||||||
|
/// </summary>
|
||||||
|
public static TTarget? MapTo<TSource, TTarget>(TSource source, bool ignoreCase = false)
|
||||||
|
where TTarget : class, new()
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var target = new TTarget();
|
||||||
|
CopyProperties(source, target, ignoreCase);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 方法调用
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调用对象的方法
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">对象实例</param>
|
||||||
|
/// <param name="methodName">方法名</param>
|
||||||
|
/// <param name="parameters">参数</param>
|
||||||
|
/// <returns>方法返回值</returns>
|
||||||
|
public static object? InvokeMethod(object obj, string methodName, params object[] parameters)
|
||||||
|
{
|
||||||
|
var type = obj.GetType();
|
||||||
|
var method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
if (method == null)
|
||||||
|
throw new InvalidOperationException($"Method '{methodName}' not found on type '{type.FullName}'");
|
||||||
|
|
||||||
|
return method.Invoke(obj, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调用静态方法
|
||||||
|
/// </summary>
|
||||||
|
public static object? InvokeStaticMethod(Type type, string methodName, params object[] parameters)
|
||||||
|
{
|
||||||
|
var method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);
|
||||||
|
|
||||||
|
if (method == null)
|
||||||
|
throw new InvalidOperationException($"Static method '{methodName}' not found on type '{type.FullName}'");
|
||||||
|
|
||||||
|
return method.Invoke(null, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 类型判断
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为可空类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNullableType(Type type)
|
||||||
|
{
|
||||||
|
return Nullable.GetUnderlyingType(type) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取可空类型的基础类型
|
||||||
|
/// </summary>
|
||||||
|
public static Type GetUnderlyingType(Type type)
|
||||||
|
{
|
||||||
|
return Nullable.GetUnderlyingType(type) ?? type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为值类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsValueType(Type type)
|
||||||
|
{
|
||||||
|
return type.IsValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为引用类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsReferenceType(Type type)
|
||||||
|
{
|
||||||
|
return !type.IsValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为数字类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNumericType(Type type)
|
||||||
|
{
|
||||||
|
type = GetUnderlyingType(type);
|
||||||
|
|
||||||
|
return type == typeof(byte) || type == typeof(sbyte) ||
|
||||||
|
type == typeof(short) || type == typeof(ushort) ||
|
||||||
|
type == typeof(int) || type == typeof(uint) ||
|
||||||
|
type == typeof(long) || type == typeof(ulong) ||
|
||||||
|
type == typeof(float) || type == typeof(double) ||
|
||||||
|
type == typeof(decimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为集合类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsCollectionType(Type type)
|
||||||
|
{
|
||||||
|
return type.IsArray ||
|
||||||
|
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) ||
|
||||||
|
type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 泛型操作
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断类型是否为泛型类型
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsGenericType(Type type)
|
||||||
|
{
|
||||||
|
return type.IsGenericType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取泛型参数类型
|
||||||
|
/// </summary>
|
||||||
|
public static Type[] GetGenericArguments(Type type)
|
||||||
|
{
|
||||||
|
return type.GetGenericArguments();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造泛型类型(例如:List<> + int => List<int>)
|
||||||
|
/// </summary>
|
||||||
|
public static Type MakeGenericType(Type genericTypeDefinition, params Type[] typeArguments)
|
||||||
|
{
|
||||||
|
return genericTypeDefinition.MakeGenericType(typeArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 缓存管理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除所有缓存
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearCache()
|
||||||
|
{
|
||||||
|
PropertyAccessorCache.Clear();
|
||||||
|
AttributeCache.Clear();
|
||||||
|
TypeCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除属性访问器缓存
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearPropertyAccessorCache()
|
||||||
|
{
|
||||||
|
PropertyAccessorCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除特性缓存
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearAttributeCache()
|
||||||
|
{
|
||||||
|
AttributeCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除类型缓存
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearTypeCache()
|
||||||
|
{
|
||||||
|
TypeCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CollectionExpand 集合扩展测试
|
||||||
|
/// 展示分页、树形结构、批处理等集合操作功能
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class CollectionExpandTests
|
||||||
|
{
|
||||||
|
#region 测试数据类
|
||||||
|
|
||||||
|
private class TestItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int? ParentId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 分页测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToPagedResult_BasicPaging_ReturnsCorrectPage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 100).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var page1 = items.ToPagedResult(1, 10);
|
||||||
|
var page2 = items.ToPagedResult(2, 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1, page1.PageIndex);
|
||||||
|
Assert.AreEqual(10, page1.PageSize);
|
||||||
|
Assert.AreEqual(100, page1.TotalCount);
|
||||||
|
Assert.AreEqual(10, page1.TotalPages);
|
||||||
|
Assert.AreEqual(10, page1.Items.Count);
|
||||||
|
Assert.AreEqual(1, page1.Items.First());
|
||||||
|
Assert.AreEqual(10, page1.Items.Last());
|
||||||
|
|
||||||
|
Assert.AreEqual(11, page2.Items.First());
|
||||||
|
Assert.AreEqual(20, page2.Items.Last());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToPagedResult_LastPage_ReturnsPartialPage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 25).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var lastPage = items.ToPagedResult(3, 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, lastPage.PageIndex);
|
||||||
|
Assert.AreEqual(5, lastPage.Items.Count); // 最后一页只有 5 个元素
|
||||||
|
Assert.AreEqual(21, lastPage.Items.First());
|
||||||
|
Assert.AreEqual(25, lastPage.Items.Last());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToPagedResult_InvalidPageIndex_ClampsToValidRange()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 50).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var zeroPage = items.ToPagedResult(0, 10); // 应该被修正为第 1 页
|
||||||
|
var negativePage = items.ToPagedResult(-1, 10); // 应该被修正为第 1 页
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1, zeroPage.PageIndex);
|
||||||
|
Assert.AreEqual(1, negativePage.PageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PagedResult_Navigation_PropertiesWorkCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 50).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var firstPage = items.ToPagedResult(1, 10);
|
||||||
|
var middlePage = items.ToPagedResult(3, 10);
|
||||||
|
var lastPage = items.ToPagedResult(5, 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(firstPage.HasPreviousPage);
|
||||||
|
Assert.IsTrue(firstPage.HasNextPage);
|
||||||
|
|
||||||
|
Assert.IsTrue(middlePage.HasPreviousPage);
|
||||||
|
Assert.IsTrue(middlePage.HasNextPage);
|
||||||
|
|
||||||
|
Assert.IsTrue(lastPage.HasPreviousPage);
|
||||||
|
Assert.IsFalse(lastPage.HasNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 树形结构测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToTree_FlatListToTree_BuildsCorrectHierarchy()
|
||||||
|
{
|
||||||
|
// Arrange: 创建平铺的列表
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "根节点1", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "根节点2", ParentId = null },
|
||||||
|
new() { Id = 3, Name = "子节点1-1", ParentId = 1 },
|
||||||
|
new() { Id = 4, Name = "子节点1-2", ParentId = 1 },
|
||||||
|
new() { Id = 5, Name = "子节点2-1", ParentId = 2 },
|
||||||
|
new() { Id = 6, Name = "孙节点1-1-1", ParentId = 3 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = items.ToTree(
|
||||||
|
idSelector: x => x.Id,
|
||||||
|
parentIdSelector: x => x.ParentId,
|
||||||
|
rootParentId: null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, tree.Count); // 两个根节点
|
||||||
|
Assert.AreEqual("根节点1", tree[0].Data.Name);
|
||||||
|
Assert.AreEqual(2, tree[0].Children.Count); // 根节点1 有两个子节点
|
||||||
|
Assert.AreEqual("子节点1-1", tree[0].Children[0].Data.Name);
|
||||||
|
Assert.AreEqual(1, tree[0].Children[0].Children.Count); // 子节点1-1 有一个孙节点
|
||||||
|
Assert.AreEqual("孙节点1-1-1", tree[0].Children[0].Children[0].Data.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToTree_BuildsCorrectLevels()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "根", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "子", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "孙", ParentId = 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = items.ToTree(x => x.Id, x => x.ParentId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(0, tree[0].Level); // 根节点 Level = 0
|
||||||
|
Assert.AreEqual(1, tree[0].Children[0].Level); // 子节点 Level = 1
|
||||||
|
Assert.AreEqual(2, tree[0].Children[0].Children[0].Level); // 孙节点 Level = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToFlatList_TreeToFlatList_PreservesAllNodes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "根", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "子1", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "子2", ParentId = 1 },
|
||||||
|
new() { Id = 4, Name = "孙", ParentId = 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tree = items.ToTree(x => x.Id, x => x.ParentId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var flatList = tree.ToFlatList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(4, flatList.Count);
|
||||||
|
Assert.AreEqual("根", flatList[0].Name);
|
||||||
|
Assert.AreEqual("子1", flatList[1].Name);
|
||||||
|
Assert.AreEqual("孙", flatList[2].Name); // 深度优先遍历
|
||||||
|
Assert.AreEqual("子2", flatList[3].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TraverseTree_ExecutesActionForAllNodes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "A", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "B", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "C", ParentId = 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tree = items.ToTree(x => x.Id, x => x.ParentId);
|
||||||
|
var visitedNames = new List<string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
tree.TraverseTree(node => visitedNames.Add(node.Data.Name));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, visitedNames.Count);
|
||||||
|
CollectionAssert.Contains(visitedNames, "A");
|
||||||
|
CollectionAssert.Contains(visitedNames, "B");
|
||||||
|
CollectionAssert.Contains(visitedNames, "C");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 批处理测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Batch_DividesIntoCorrectBatches()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 10).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var batches = items.Batch(3).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(4, batches.Count); // 10 个元素,每批 3 个,共 4 批
|
||||||
|
Assert.AreEqual(3, batches[0].Count());
|
||||||
|
Assert.AreEqual(3, batches[1].Count());
|
||||||
|
Assert.AreEqual(3, batches[2].Count());
|
||||||
|
Assert.AreEqual(1, batches[3].Count()); // 最后一批只有 1 个
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Batch_EmptyCollection_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var batches = items.Batch(5).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(0, batches.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ForEachBatch_ExecutesActionForEachBatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 25).ToList();
|
||||||
|
var processedBatches = new List<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
items.ForEachBatch(10, batch =>
|
||||||
|
{
|
||||||
|
processedBatches.Add(batch.Count());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, processedBatches.Count);
|
||||||
|
Assert.AreEqual(10, processedBatches[0]);
|
||||||
|
Assert.AreEqual(10, processedBatches[1]);
|
||||||
|
Assert.AreEqual(5, processedBatches[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 空集合安全操作测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void OrEmpty_NullCollection_ReturnsEmptyCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
List<int>? nullList = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = nullList.OrEmpty();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(0, result.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void OrDefault_EmptyCollection_ReturnsDefaultValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var emptyList = new List<int>();
|
||||||
|
var defaultList = new List<int> { 1, 2, 3 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = emptyList.OrDefault(defaultList);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, result.Count());
|
||||||
|
CollectionAssert.AreEqual(defaultList, result.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsNullOrEmpty_VariousInputs_ReturnsCorrectResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
List<int>? nullList = null;
|
||||||
|
var emptyList = new List<int>();
|
||||||
|
var nonEmptyList = new List<int> { 1 };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(nullList.IsNullOrEmpty());
|
||||||
|
Assert.IsTrue(emptyList.IsNullOrEmpty());
|
||||||
|
Assert.IsFalse(nonEmptyList.IsNullOrEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void HasItems_VariousInputs_ReturnsCorrectResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
List<int>? nullList = null;
|
||||||
|
var emptyList = new List<int>();
|
||||||
|
var nonEmptyList = new List<int> { 1 };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsFalse(nullList.HasItems());
|
||||||
|
Assert.IsFalse(emptyList.HasItems());
|
||||||
|
Assert.IsTrue(nonEmptyList.HasItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DistinctBy 测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DistinctBy_RemovesDuplicatesByKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "A" },
|
||||||
|
new() { Id = 2, Name = "B" },
|
||||||
|
new() { Id = 3, Name = "A" }, // 重复的 Name
|
||||||
|
new() { Id = 4, Name = "C" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act (.NET 6+ 内置 DistinctBy)
|
||||||
|
var distinct = items.DistinctBy(x => x.Name).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, distinct.Count);
|
||||||
|
Assert.AreEqual(1, distinct[0].Id); // 保留第一个 "A"
|
||||||
|
Assert.AreEqual(2, distinct[1].Id);
|
||||||
|
Assert.AreEqual(4, distinct[2].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 其他实用扩展测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ForEach_ExecutesActionForAllElements()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<int> { 1, 2, 3 };
|
||||||
|
var sum = 0;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
items.ForEach(x => sum += x);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(6, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ForEach_WithIndex_ProvidesCorrectIndex()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<string> { "A", "B", "C" };
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
items.ForEach((item, index) => results.Add($"{index}:{item}"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
CollectionAssert.AreEqual(new[] { "0:A", "1:B", "2:C" }, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void JoinString_ConcatenatesWithSeparator()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new[] { "apple", "banana", "cherry" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = items.JoinString(", ");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("apple, banana, cherry", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Shuffle_ChangesOrder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 100).ToList();
|
||||||
|
var original = items.ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var shuffled = items.Shuffle().ToList();
|
||||||
|
|
||||||
|
// Assert: 打乱后的顺序应该不同(理论上有极小概率相同)
|
||||||
|
Assert.AreEqual(original.Count, shuffled.Count);
|
||||||
|
// 检查是否至少有一些元素位置变化
|
||||||
|
var differentPositions = original.Where((item, index) => shuffled[index] != item).Count();
|
||||||
|
Assert.IsTrue(differentPositions > 10); // 至少 10% 的元素位置不同
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TakeRandom_ReturnsSpecifiedCount()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 100).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var random10 = items.TakeRandom(10).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(10, random10.Count);
|
||||||
|
Assert.IsTrue(random10.All(x => items.Contains(x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Split_DividesIntoParts()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = Enumerable.Range(1, 10).ToList();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parts = items.Split(3).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, parts.Count);
|
||||||
|
Assert.AreEqual(4, parts[0].Count()); // 10 / 3 = 向上取整 4
|
||||||
|
Assert.AreEqual(4, parts[1].Count());
|
||||||
|
Assert.AreEqual(2, parts[2].Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AllUnique_ChecksUniqueness()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var uniqueList = new[] { 1, 2, 3, 4 };
|
||||||
|
var duplicateList = new[] { 1, 2, 2, 3 };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(uniqueList.AllUnique());
|
||||||
|
Assert.IsFalse(duplicateList.AllUnique());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void HasDuplicates_DetectsDuplicates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var uniqueList = new[] { 1, 2, 3, 4 };
|
||||||
|
var duplicateList = new[] { 1, 2, 2, 3 };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsFalse(uniqueList.HasDuplicates());
|
||||||
|
Assert.IsTrue(duplicateList.HasDuplicates());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetDuplicates_ReturnsOnlyDuplicates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new[] { 1, 2, 2, 3, 3, 3, 4 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var duplicates = items.GetDuplicates().ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, duplicates.Count);
|
||||||
|
CollectionAssert.Contains(duplicates, 2);
|
||||||
|
CollectionAssert.Contains(duplicates, 3);
|
||||||
|
CollectionAssert.DoesNotContain(duplicates, 1);
|
||||||
|
CollectionAssert.DoesNotContain(duplicates, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Interleave_MergesAlternately()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var first = new[] { 1, 3, 5 };
|
||||||
|
var second = new[] { 2, 4, 6 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = first.Interleave(second).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 5, 6 }, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Interleave_DifferentLengths_MergesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var first = new[] { 1, 3 };
|
||||||
|
var second = new[] { 2, 4, 6, 8 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = first.Interleave(second).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 6, 8 }, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void WhereNotNull_FiltersNullValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<string?> { "A", null, "B", null, "C" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = items.WhereNotNull().ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, result.Count);
|
||||||
|
CollectionAssert.AreEqual(new[] { "A", "B", "C" }, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToDictionaryList_GroupsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "A", ParentId = 1 },
|
||||||
|
new() { Id = 2, Name = "B", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "C", ParentId = 2 },
|
||||||
|
new() { Id = 4, Name = "D", ParentId = 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var grouped = items.ToDictionaryList(
|
||||||
|
x => x.ParentId!.Value,
|
||||||
|
x => x.Name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, grouped.Count);
|
||||||
|
Assert.AreEqual(2, grouped[1].Count);
|
||||||
|
CollectionAssert.Contains(grouped[1], "A");
|
||||||
|
CollectionAssert.Contains(grouped[1], "B");
|
||||||
|
Assert.AreEqual(2, grouped[2].Count);
|
||||||
|
CollectionAssert.Contains(grouped[2], "C");
|
||||||
|
CollectionAssert.Contains(grouped[2], "D");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_PaginationWithSearch()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟产品列表搜索和分页
|
||||||
|
var products = Enumerable.Range(1, 100)
|
||||||
|
.Select(i => new { Id = i, Name = $"Product {i}", Price = i * 10 })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Act: 搜索价格大于 500 的产品,并分页
|
||||||
|
var searchResults = products
|
||||||
|
.Where(p => p.Price > 500)
|
||||||
|
.ToPagedResult(1, 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(10, searchResults.Items.Count);
|
||||||
|
Assert.IsTrue(searchResults.Items.All(p => p.Price > 500));
|
||||||
|
Assert.IsTrue(searchResults.TotalCount > 40); // 总共有 50+ 个符合条件的产品
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_OrganizationTree()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟组织架构树
|
||||||
|
var departments = new List<TestItem>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Name = "总公司", ParentId = null },
|
||||||
|
new() { Id = 2, Name = "技术部", ParentId = 1 },
|
||||||
|
new() { Id = 3, Name = "市场部", ParentId = 1 },
|
||||||
|
new() { Id = 4, Name = "前端组", ParentId = 2 },
|
||||||
|
new() { Id = 5, Name = "后端组", ParentId = 2 },
|
||||||
|
new() { Id = 6, Name = "运营组", ParentId = 3 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act: 构建组织树
|
||||||
|
var tree = departments.ToTree(x => x.Id, x => x.ParentId);
|
||||||
|
|
||||||
|
// Assert: 验证树形结构
|
||||||
|
Assert.AreEqual(1, tree.Count); // 1 个根节点(总公司)
|
||||||
|
Assert.AreEqual(2, tree[0].Children.Count); // 2 个一级部门
|
||||||
|
Assert.AreEqual(2, tree[0].Children[0].Children.Count); // 技术部有 2 个组
|
||||||
|
Assert.AreEqual("前端组", tree[0].Children[0].Children[0].Data.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_BatchProcessing()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟批量导入数据
|
||||||
|
var records = Enumerable.Range(1, 1000).ToList();
|
||||||
|
var processedCount = 0;
|
||||||
|
|
||||||
|
// Act: 每批处理 100 条记录
|
||||||
|
records.ForEachBatch(100, batch =>
|
||||||
|
{
|
||||||
|
// 模拟批量插入数据库
|
||||||
|
processedCount += batch.Count();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1000, processedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_DataCleaning()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟清洗数据(去除 null、去重、过滤)
|
||||||
|
var rawData = new List<string?> { "A", null, "B", "A", null, "C", "B", "D" };
|
||||||
|
|
||||||
|
// Act: 数据清洗流水线
|
||||||
|
var cleanedData = rawData
|
||||||
|
.WhereNotNull() // 去除 null
|
||||||
|
.DistinctBy(x => x) // 去重
|
||||||
|
.Where(x => x != "D") // 过滤不需要的数据
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(3, cleanedData.Count);
|
||||||
|
CollectionAssert.AreEqual(new[] { "A", "B", "C" }, cleanedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EnumHelper 枚举工具测试
|
||||||
|
/// 展示枚举解析、描述获取、标志位操作等功能
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class EnumHelperTests
|
||||||
|
{
|
||||||
|
#region 测试枚举定义
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态枚举
|
||||||
|
/// </summary>
|
||||||
|
public enum OrderStatus
|
||||||
|
{
|
||||||
|
[System.ComponentModel.Description("待支付")]
|
||||||
|
Pending = 0,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("已支付")]
|
||||||
|
Paid = 1,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("已发货")]
|
||||||
|
Shipped = 2,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("已完成")]
|
||||||
|
Completed = 3,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("已取消")]
|
||||||
|
Cancelled = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户权限枚举(标志位)
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum UserPermissions
|
||||||
|
{
|
||||||
|
[System.ComponentModel.Description("无权限")]
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("读取权限")]
|
||||||
|
Read = 1,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("写入权限")]
|
||||||
|
Write = 2,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("删除权限")]
|
||||||
|
Delete = 4,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("管理权限")]
|
||||||
|
Admin = 8,
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("全部权限")]
|
||||||
|
All = Read | Write | Delete | Admin
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志级别(无描述)
|
||||||
|
/// </summary>
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Debug = 1,
|
||||||
|
Info = 2,
|
||||||
|
Warning = 3,
|
||||||
|
Error = 4,
|
||||||
|
Fatal = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举解析测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Parse_ValidString_ReturnsEnum()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result1 = EnumHelper.Parse<OrderStatus>("Paid");
|
||||||
|
var result2 = EnumHelper.Parse<OrderStatus>("paid"); // 忽略大小写
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result1);
|
||||||
|
Assert.AreEqual(OrderStatus.Paid, result1.Value);
|
||||||
|
Assert.IsNotNull(result2);
|
||||||
|
Assert.AreEqual(OrderStatus.Paid, result2.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Parse_InvalidString_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.Parse<OrderStatus>("InvalidValue");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNull(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ParseOrDefault_InvalidString_ReturnsDefaultValue()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.ParseOrDefault("InvalidValue", OrderStatus.Pending);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(OrderStatus.Pending, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ParseFromInt_ValidValue_ReturnsEnum()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.ParseFromInt<OrderStatus>(2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(OrderStatus.Shipped, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ParseFromInt_InvalidValue_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.ParseFromInt<OrderStatus>(999);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNull(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ParseFromDescription_ValidDescription_ReturnsEnum()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.ParseFromDescription<OrderStatus>("已支付");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(OrderStatus.Paid, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ParseFromDescription_InvalidDescription_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = EnumHelper.ParseFromDescription<OrderStatus>("不存在的状态");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNull(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举描述测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetDescription_WithDescriptionAttribute_ReturnsDescription()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var status = OrderStatus.Paid;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var description = status.GetDescription();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("已支付", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetDescription_WithoutDescriptionAttribute_ReturnsEnumName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var level = LogLevel.Error;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var description = level.GetDescription();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Error", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetAllDescriptions_ReturnsAllDescriptions()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var descriptions = EnumHelper.GetAllDescriptions<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, descriptions.Count);
|
||||||
|
Assert.AreEqual("待支付", descriptions[OrderStatus.Pending]);
|
||||||
|
Assert.AreEqual("已支付", descriptions[OrderStatus.Paid]);
|
||||||
|
Assert.AreEqual("已完成", descriptions[OrderStatus.Completed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举列表测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetValues_ReturnsAllValues()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var values = EnumHelper.GetValues<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, values.Length);
|
||||||
|
CollectionAssert.Contains(values, OrderStatus.Pending);
|
||||||
|
CollectionAssert.Contains(values, OrderStatus.Paid);
|
||||||
|
CollectionAssert.Contains(values, OrderStatus.Shipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetNames_ReturnsAllNames()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var names = EnumHelper.GetNames<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, names.Length);
|
||||||
|
CollectionAssert.Contains(names, "Pending");
|
||||||
|
CollectionAssert.Contains(names, "Paid");
|
||||||
|
CollectionAssert.Contains(names, "Shipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetOptions_ReturnsListForDropdown()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var options = EnumHelper.GetOptions<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, options.Count);
|
||||||
|
|
||||||
|
var paidOption = options.First(o => o.Value == 1);
|
||||||
|
Assert.AreEqual("Paid", paidOption.Name);
|
||||||
|
Assert.AreEqual("已支付", paidOption.Description);
|
||||||
|
Assert.AreEqual("已支付", paidOption.DisplayText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetDictionary_ReturnsValueNamePairs()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var dict = EnumHelper.GetDictionary<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, dict.Count);
|
||||||
|
Assert.AreEqual("Paid", dict[1]);
|
||||||
|
Assert.AreEqual("Shipped", dict[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetDictionaryWithDescription_ReturnsValueDescriptionPairs()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var dict = EnumHelper.GetDictionaryWithDescription<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, dict.Count);
|
||||||
|
Assert.AreEqual("已支付", dict[1]);
|
||||||
|
Assert.AreEqual("已发货", dict[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsDefined_ValidValue_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(EnumHelper.IsDefined<OrderStatus>(1));
|
||||||
|
Assert.IsTrue(EnumHelper.IsDefined<OrderStatus>("Paid"));
|
||||||
|
Assert.IsTrue(EnumHelper.IsDefined(OrderStatus.Paid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsDefined_InvalidValue_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsFalse(EnumHelper.IsDefined<OrderStatus>(999));
|
||||||
|
Assert.IsFalse(EnumHelper.IsDefined<OrderStatus>("InvalidStatus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 标志位枚举操作测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void HasFlag_ChecksFlagCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var permissions = UserPermissions.Read | UserPermissions.Write;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(permissions.HasFlag(UserPermissions.Read));
|
||||||
|
Assert.IsTrue(permissions.HasFlag(UserPermissions.Write));
|
||||||
|
Assert.IsFalse(permissions.HasFlag(UserPermissions.Delete));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AddFlag_AddsNewFlag()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var permissions = UserPermissions.Read;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var newPermissions = permissions.AddFlag(UserPermissions.Write);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(newPermissions.HasFlag(UserPermissions.Read));
|
||||||
|
Assert.IsTrue(newPermissions.HasFlag(UserPermissions.Write));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RemoveFlag_RemovesExistingFlag()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var permissions = UserPermissions.Read | UserPermissions.Write | UserPermissions.Delete;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var newPermissions = permissions.RemoveFlag(UserPermissions.Write);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(newPermissions.HasFlag(UserPermissions.Read));
|
||||||
|
Assert.IsFalse(newPermissions.HasFlag(UserPermissions.Write));
|
||||||
|
Assert.IsTrue(newPermissions.HasFlag(UserPermissions.Delete));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToggleFlag_TogglesFlag()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var permissions = UserPermissions.Read;
|
||||||
|
|
||||||
|
// Act: 切换 Write(添加)
|
||||||
|
var withWrite = permissions.ToggleFlag(UserPermissions.Write);
|
||||||
|
Assert.IsTrue(withWrite.HasFlag(UserPermissions.Write));
|
||||||
|
|
||||||
|
// Act: 再次切换 Write(移除)
|
||||||
|
var withoutWrite = withWrite.ToggleFlag(UserPermissions.Write);
|
||||||
|
Assert.IsFalse(withoutWrite.HasFlag(UserPermissions.Write));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetFlags_ReturnsAllSetFlags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var permissions = UserPermissions.Read | UserPermissions.Write | UserPermissions.Admin;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var flags = permissions.GetFlags().ToList();
|
||||||
|
|
||||||
|
// Assert: None 不应该包含在内(因为值是 0)
|
||||||
|
Assert.IsTrue(flags.Contains(UserPermissions.Read));
|
||||||
|
Assert.IsTrue(flags.Contains(UserPermissions.Write));
|
||||||
|
Assert.IsTrue(flags.Contains(UserPermissions.Admin));
|
||||||
|
Assert.IsFalse(flags.Contains(UserPermissions.Delete));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CombineFlags_CombinesMultipleFlags()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var combined = EnumHelper.CombineFlags(
|
||||||
|
UserPermissions.Read,
|
||||||
|
UserPermissions.Write,
|
||||||
|
UserPermissions.Delete
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(combined.HasFlag(UserPermissions.Read));
|
||||||
|
Assert.IsTrue(combined.HasFlag(UserPermissions.Write));
|
||||||
|
Assert.IsTrue(combined.HasFlag(UserPermissions.Delete));
|
||||||
|
Assert.IsFalse(combined.HasFlag(UserPermissions.Admin));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举转换测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToInt_ConvertsToInteger()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var status = OrderStatus.Paid;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var intValue = status.ToInt();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1, intValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToLong_ConvertsToLong()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var status = OrderStatus.Shipped;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var longValue = status.ToLong();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2L, longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToEnum_ConvertsIntToEnum()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
int value = 3;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var status = value.ToEnum<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(OrderStatus.Completed, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举信息测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetCount_ReturnsEnumCount()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var count = EnumHelper.GetCount<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetMinValue_ReturnsSmallestValue()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var min = EnumHelper.GetMinValue<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(OrderStatus.Pending, min);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetMaxValue_ReturnsLargestValue()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var max = EnumHelper.GetMaxValue<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(OrderStatus.Cancelled, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsFlagsEnum_DetectsFlagsAttribute()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var isFlagsEnum = EnumHelper.IsFlagsEnum<UserPermissions>();
|
||||||
|
var isNotFlagsEnum = EnumHelper.IsFlagsEnum<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(isFlagsEnum);
|
||||||
|
Assert.IsFalse(isNotFlagsEnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 枚举比较测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Equals_ComparesEnumValues()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(EnumHelper.Equals(OrderStatus.Paid, OrderStatus.Paid));
|
||||||
|
Assert.IsFalse(EnumHelper.Equals(OrderStatus.Paid, OrderStatus.Pending));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void In_ChecksIfValueInList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var status = OrderStatus.Paid;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(status.In(OrderStatus.Pending, OrderStatus.Paid, OrderStatus.Shipped));
|
||||||
|
Assert.IsFalse(status.In(OrderStatus.Completed, OrderStatus.Cancelled));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void NotIn_ChecksIfValueNotInList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var status = OrderStatus.Paid;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsFalse(status.NotIn(OrderStatus.Pending, OrderStatus.Paid, OrderStatus.Shipped));
|
||||||
|
Assert.IsTrue(status.NotIn(OrderStatus.Completed, OrderStatus.Cancelled));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 随机枚举测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetRandom_ReturnsValidEnum()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var random = EnumHelper.GetRandom<OrderStatus>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(EnumHelper.IsDefined(random));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetRandomExcept_ExcludesSpecifiedValues()
|
||||||
|
{
|
||||||
|
// Act: 多次获取随机值,确保不包含排除的值
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var random = EnumHelper.GetRandomExcept(OrderStatus.Pending, OrderStatus.Cancelled);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(random.NotIn(OrderStatus.Pending, OrderStatus.Cancelled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_OrderStatusWorkflow()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟订单状态流转
|
||||||
|
var currentStatus = OrderStatus.Pending;
|
||||||
|
|
||||||
|
// Act: 支付订单
|
||||||
|
currentStatus = OrderStatus.Paid;
|
||||||
|
Assert.AreEqual("已支付", currentStatus.GetDescription());
|
||||||
|
|
||||||
|
// Act: 发货
|
||||||
|
currentStatus = OrderStatus.Shipped;
|
||||||
|
Assert.AreEqual("已发货", currentStatus.GetDescription());
|
||||||
|
|
||||||
|
// Assert: 检查状态是否在允许的范围内
|
||||||
|
var validStatuses = new[] { OrderStatus.Shipped, OrderStatus.Completed };
|
||||||
|
Assert.IsTrue(currentStatus.In(validStatuses));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_PermissionManagement()
|
||||||
|
{
|
||||||
|
// Arrange: 初始化普通用户权限
|
||||||
|
var userPermissions = UserPermissions.Read;
|
||||||
|
|
||||||
|
// Act: 升级为编辑权限
|
||||||
|
userPermissions = userPermissions.AddFlag(UserPermissions.Write);
|
||||||
|
Assert.IsTrue(userPermissions.HasFlag(UserPermissions.Write));
|
||||||
|
|
||||||
|
// Act: 再添加删除权限
|
||||||
|
userPermissions = userPermissions.AddFlag(UserPermissions.Delete);
|
||||||
|
|
||||||
|
// Assert: 验证权限
|
||||||
|
var grantedPermissions = userPermissions.GetFlags().ToList();
|
||||||
|
Assert.AreEqual(3, grantedPermissions.Count);
|
||||||
|
Assert.IsTrue(grantedPermissions.Contains(UserPermissions.Read));
|
||||||
|
Assert.IsTrue(grantedPermissions.Contains(UserPermissions.Write));
|
||||||
|
Assert.IsTrue(grantedPermissions.Contains(UserPermissions.Delete));
|
||||||
|
|
||||||
|
// Act: 撤销写入权限
|
||||||
|
userPermissions = userPermissions.RemoveFlag(UserPermissions.Write);
|
||||||
|
Assert.IsFalse(userPermissions.HasFlag(UserPermissions.Write));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_DropdownBinding()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟为前端下拉框准备数据
|
||||||
|
var statusOptions = EnumHelper.GetOptions<OrderStatus>();
|
||||||
|
|
||||||
|
// Act: 转换为前端所需的格式
|
||||||
|
var dropdownData = statusOptions.Select(o => new
|
||||||
|
{
|
||||||
|
Value = o.Value,
|
||||||
|
Text = o.DisplayText
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(5, dropdownData.Count);
|
||||||
|
Assert.AreEqual("待支付", dropdownData[0].Text);
|
||||||
|
Assert.AreEqual(0, dropdownData[0].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_StatusValidation()
|
||||||
|
{
|
||||||
|
// Arrange: 从前端接收状态值
|
||||||
|
var statusFromFrontend = 2;
|
||||||
|
|
||||||
|
// Act: 验证并转换
|
||||||
|
if (EnumHelper.IsDefined<OrderStatus>(statusFromFrontend))
|
||||||
|
{
|
||||||
|
var status = statusFromFrontend.ToEnum<OrderStatus>();
|
||||||
|
var description = status.GetDescription();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(OrderStatus.Shipped, status);
|
||||||
|
Assert.AreEqual("已发货", description);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Fail("Invalid status value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_ParseUserInput()
|
||||||
|
{
|
||||||
|
// Arrange: 用户输入的状态名称
|
||||||
|
var userInput = "paid"; // 可能是小写
|
||||||
|
|
||||||
|
// Act: 解析
|
||||||
|
var status = EnumHelper.Parse<OrderStatus>(userInput);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(status);
|
||||||
|
Assert.AreEqual(OrderStatus.Paid, status.Value);
|
||||||
|
Assert.AreEqual("已支付", status.Value.GetDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ReflectionHelper 反射工具测试
|
||||||
|
/// 展示类型扫描、属性访问、特性读取等功能
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class ReflectionHelperTests
|
||||||
|
{
|
||||||
|
#region 测试类定义
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseEntity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ITestService
|
||||||
|
{
|
||||||
|
void Execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试特性
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||||
|
public class TestAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public TestAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试实体类
|
||||||
|
/// </summary>
|
||||||
|
[Test("UserEntity")]
|
||||||
|
public class User : BaseEntity
|
||||||
|
{
|
||||||
|
[Test("UserName")]
|
||||||
|
[System.ComponentModel.Description("用户名")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[System.ComponentModel.Description("用户年龄")]
|
||||||
|
public int Age { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public void SayHello()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Hello, I'm {Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetVersion()
|
||||||
|
{
|
||||||
|
return "1.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calculate(int a, int b)
|
||||||
|
{
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int Age { get; set; }
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class TestService : ITestService
|
||||||
|
{
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Executing test service");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 类型扫描测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void FindSubclassesOf_FindsAllSubclasses()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var subclasses = ReflectionHelper.FindSubclassesOf<BaseEntity>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(subclasses.Count > 0);
|
||||||
|
Assert.IsTrue(subclasses.Any(t => t == typeof(User)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void FindImplementationsOf_FindsAllImplementations()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var implementations = ReflectionHelper.FindImplementationsOf<ITestService>(assembly);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(implementations.Count > 0);
|
||||||
|
Assert.IsTrue(implementations.Any(t => t == typeof(TestService)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void FindTypesWithAttribute_FindsTypesWithAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var types = ReflectionHelper.FindTypesWithAttribute<TestAttribute>(assembly);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(types.Count > 0);
|
||||||
|
Assert.IsTrue(types.Any(t => t == typeof(User)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 属性访问测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetPropertyValue_ReturnsCorrectValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User { Name = "Alice", Age = 25 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var name = ReflectionHelper.GetPropertyValue(user, nameof(User.Name));
|
||||||
|
var age = ReflectionHelper.GetPropertyValue(user, nameof(User.Age));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Alice", name);
|
||||||
|
Assert.AreEqual(25, age);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void SetPropertyValue_SetsValueCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.SetPropertyValue(user, nameof(User.Name), "Bob");
|
||||||
|
ReflectionHelper.SetPropertyValue(user, nameof(User.Age), 30);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Bob", user.Name);
|
||||||
|
Assert.AreEqual(30, user.Age);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetPropertyValue_Performance_IsFasterThanDirectReflection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User { Name = "Alice", Age = 25 };
|
||||||
|
const int iterations = 10000;
|
||||||
|
|
||||||
|
// 预热缓存
|
||||||
|
ReflectionHelper.GetPropertyValue(user, nameof(User.Name));
|
||||||
|
|
||||||
|
// Act: 使用 ReflectionHelper(有缓存)
|
||||||
|
var sw1 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
for (int i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
_ = ReflectionHelper.GetPropertyValue(user, nameof(User.Name));
|
||||||
|
}
|
||||||
|
sw1.Stop();
|
||||||
|
|
||||||
|
// Act: 使用原生反射(无缓存)
|
||||||
|
var propertyInfo = typeof(User).GetProperty(nameof(User.Name))!;
|
||||||
|
var sw2 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
for (int i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
_ = propertyInfo.GetValue(user);
|
||||||
|
}
|
||||||
|
sw2.Stop();
|
||||||
|
|
||||||
|
// Assert: ReflectionHelper 应该比直接反射快(由于缓存和表达式树)
|
||||||
|
Console.WriteLine($"ReflectionHelper: {sw1.ElapsedMilliseconds}ms");
|
||||||
|
Console.WriteLine($"Direct Reflection: {sw2.ElapsedMilliseconds}ms");
|
||||||
|
// 注意:首次运行可能因为 JIT 编译而不准确,多次运行后 ReflectionHelper 应该更快
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetPropertyNames_ReturnsAllPropertyNames()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var propertyNames = ReflectionHelper.GetPropertyNames<User>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(propertyNames.Count >= 5);
|
||||||
|
CollectionAssert.Contains(propertyNames, nameof(User.Name));
|
||||||
|
CollectionAssert.Contains(propertyNames, nameof(User.Age));
|
||||||
|
CollectionAssert.Contains(propertyNames, nameof(User.Email));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetProperties_ReturnsAllProperties()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var properties = ReflectionHelper.GetProperties<User>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(properties.Length >= 5);
|
||||||
|
Assert.IsTrue(properties.Any(p => p.Name == nameof(User.Name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 特性读取测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetAttribute_ReturnsAttribute()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var attribute = ReflectionHelper.GetAttribute<TestAttribute>(typeof(User));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(attribute);
|
||||||
|
Assert.AreEqual("UserEntity", attribute.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetAttribute_FromProperty_ReturnsAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = typeof(User).GetProperty(nameof(User.Name))!;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var testAttr = ReflectionHelper.GetAttribute<TestAttribute>(property);
|
||||||
|
var descAttr = ReflectionHelper.GetAttribute<System.ComponentModel.DescriptionAttribute>(property);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(testAttr);
|
||||||
|
Assert.AreEqual("UserName", testAttr.Name);
|
||||||
|
Assert.IsNotNull(descAttr);
|
||||||
|
Assert.AreEqual("用户名", descAttr.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void HasAttribute_ChecksAttributeExistence()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.HasAttribute<TestAttribute>(typeof(User)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.HasAttribute<TestAttribute>(typeof(UserDto)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetAttribute_UsesCaching()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(User);
|
||||||
|
|
||||||
|
// Act: 多次获取应该使用缓存
|
||||||
|
var attr1 = ReflectionHelper.GetAttribute<TestAttribute>(type);
|
||||||
|
var attr2 = ReflectionHelper.GetAttribute<TestAttribute>(type);
|
||||||
|
|
||||||
|
// Assert: 应该返回相同的实例(来自缓存)
|
||||||
|
Assert.IsNotNull(attr1);
|
||||||
|
Assert.IsNotNull(attr2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 创建实例测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CreateInstance_Generic_CreatesInstance()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var user = ReflectionHelper.CreateInstance<User>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(user);
|
||||||
|
Assert.IsInstanceOfType(user, typeof(User));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CreateInstance_ByType_CreatesInstance()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var user = ReflectionHelper.CreateInstance(typeof(User));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(user);
|
||||||
|
Assert.IsInstanceOfType(user, typeof(User));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TryCreateInstance_SuccessfulCreation_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var success = ReflectionHelper.TryCreateInstance<User>(out var user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(success);
|
||||||
|
Assert.IsNotNull(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 属性复制测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CopyProperties_CopiesSameNameProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new User
|
||||||
|
{
|
||||||
|
Name = "Alice",
|
||||||
|
Age = 25,
|
||||||
|
Email = "alice@example.com"
|
||||||
|
};
|
||||||
|
var target = new UserDto();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.CopyProperties(source, target);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Alice", target.Name);
|
||||||
|
Assert.AreEqual(25, target.Age);
|
||||||
|
Assert.AreEqual("alice@example.com", target.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MapTo_CreatesNewInstanceAndCopiesProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new User
|
||||||
|
{
|
||||||
|
Name = "Bob",
|
||||||
|
Age = 30,
|
||||||
|
Email = "bob@example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dto = ReflectionHelper.MapTo<User, UserDto>(source);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(dto);
|
||||||
|
Assert.AreEqual("Bob", dto.Name);
|
||||||
|
Assert.AreEqual(30, dto.Age);
|
||||||
|
Assert.AreEqual("bob@example.com", dto.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CopyProperties_IgnoreCase_CopiesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new { name = "Alice", AGE = 25 };
|
||||||
|
var target = new UserDto();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.CopyProperties(source, target, ignoreCase: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Alice", target.Name);
|
||||||
|
Assert.AreEqual(25, target.Age);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 方法调用测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void InvokeMethod_CallsInstanceMethod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User { Name = "Alice" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReflectionHelper.InvokeMethod(user, nameof(User.Calculate), 10, 20);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(30, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void InvokeStaticMethod_CallsStaticMethod()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = ReflectionHelper.InvokeStaticMethod(typeof(User), nameof(User.GetVersion));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual("1.0.0", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(InvalidOperationException))]
|
||||||
|
public void InvokeMethod_NonExistentMethod_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.InvokeMethod(user, "NonExistentMethod");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 类型判断测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsNullableType_DetectsNullableTypes()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsNullableType(typeof(int?)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsNullableType(typeof(int)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsNullableType(typeof(string)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetUnderlyingType_ReturnsCorrectType()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var underlyingType = ReflectionHelper.GetUnderlyingType(typeof(int?));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(typeof(int), underlyingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValueType_DistinguishesValueTypes()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsValueType(typeof(int)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsValueType(typeof(DateTime)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsValueType(typeof(string)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsValueType(typeof(User)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsNumericType_DetectsNumericTypes()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsNumericType(typeof(int)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsNumericType(typeof(double)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsNumericType(typeof(decimal)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsNumericType(typeof(int?)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsNumericType(typeof(string)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsNumericType(typeof(DateTime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsCollectionType_DetectsCollections()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsCollectionType(typeof(int[])));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsCollectionType(typeof(List<int>)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsCollectionType(typeof(IEnumerable<int>)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsCollectionType(typeof(int)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsCollectionType(typeof(string)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 泛型操作测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsGenericType_DetectsGenericTypes()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsGenericType(typeof(List<int>)));
|
||||||
|
Assert.IsTrue(ReflectionHelper.IsGenericType(typeof(Dictionary<string, int>)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsGenericType(typeof(int)));
|
||||||
|
Assert.IsFalse(ReflectionHelper.IsGenericType(typeof(string)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetGenericArguments_ReturnsTypeArguments()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var args = ReflectionHelper.GetGenericArguments(typeof(Dictionary<string, int>));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, args.Length);
|
||||||
|
Assert.AreEqual(typeof(string), args[0]);
|
||||||
|
Assert.AreEqual(typeof(int), args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MakeGenericType_ConstructsGenericType()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var listType = ReflectionHelper.MakeGenericType(typeof(List<>), typeof(string));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(typeof(List<string>), listType);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 缓存管理测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ClearCache_ClearsAllCaches()
|
||||||
|
{
|
||||||
|
// Arrange: 先访问一些缓存
|
||||||
|
ReflectionHelper.GetPropertyValue(new User { Name = "Test" }, nameof(User.Name));
|
||||||
|
ReflectionHelper.GetAttribute<TestAttribute>(typeof(User));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.ClearCache();
|
||||||
|
|
||||||
|
// Assert: 缓存应该被清空(无法直接验证,但不应抛异常)
|
||||||
|
Assert.IsTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ClearPropertyAccessorCache_ClearsPropertyCache()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ReflectionHelper.GetPropertyValue(new User { Name = "Test" }, nameof(User.Name));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ReflectionHelper.ClearPropertyAccessorCache();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_PluginScanning()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟插件系统,扫描所有实现 ITestService 的类
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
|
// Act: 查找所有插件
|
||||||
|
var plugins = ReflectionHelper.FindImplementationsOf<ITestService>(assembly);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(plugins.Count > 0);
|
||||||
|
|
||||||
|
// Act: 实例化并执行插件
|
||||||
|
foreach (var pluginType in plugins)
|
||||||
|
{
|
||||||
|
if (ReflectionHelper.CreateInstance(pluginType) is ITestService plugin)
|
||||||
|
{
|
||||||
|
plugin.Execute(); // 输出:Executing test service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_AutoMapper()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟 DTO 映射
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "Alice",
|
||||||
|
Age = 25,
|
||||||
|
Email = "alice@example.com",
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act: 自动映射到 DTO
|
||||||
|
var dto = ReflectionHelper.MapTo<User, UserDto>(user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(dto);
|
||||||
|
Assert.AreEqual("Alice", dto.Name);
|
||||||
|
Assert.AreEqual(25, dto.Age);
|
||||||
|
Assert.AreEqual("alice@example.com", dto.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_DynamicPropertyAccess()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟动态表单数据
|
||||||
|
var user = new User { Name = "Bob", Age = 30 };
|
||||||
|
var propertyUpdates = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ nameof(User.Name), "Updated Bob" },
|
||||||
|
{ nameof(User.Age), 31 },
|
||||||
|
{ nameof(User.Email), "bob@updated.com" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act: 动态更新属性
|
||||||
|
foreach (var (propertyName, value) in propertyUpdates)
|
||||||
|
{
|
||||||
|
ReflectionHelper.SetPropertyValue(user, propertyName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Updated Bob", user.Name);
|
||||||
|
Assert.AreEqual(31, user.Age);
|
||||||
|
Assert.AreEqual("bob@updated.com", user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_AttributeBasedValidation()
|
||||||
|
{
|
||||||
|
// Arrange: 读取属性上的描述特性用于验证消息
|
||||||
|
var properties = typeof(User).GetProperties();
|
||||||
|
var validationMessages = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
// Act: 收集所有带描述的属性
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var description = ReflectionHelper.GetAttribute<System.ComponentModel.DescriptionAttribute>(property);
|
||||||
|
if (description != null)
|
||||||
|
{
|
||||||
|
validationMessages[property.Name] = $"{description.Description}不能为空";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(validationMessages.ContainsKey(nameof(User.Name)));
|
||||||
|
Assert.AreEqual("用户名不能为空", validationMessages[nameof(User.Name)]);
|
||||||
|
Assert.IsTrue(validationMessages.ContainsKey(nameof(User.Age)));
|
||||||
|
Assert.AreEqual("用户年龄不能为空", validationMessages[nameof(User.Age)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_TypeInspection()
|
||||||
|
{
|
||||||
|
// Arrange: 检查类型信息
|
||||||
|
var type = typeof(User);
|
||||||
|
|
||||||
|
// Act: 收集类型信息
|
||||||
|
var info = new
|
||||||
|
{
|
||||||
|
IsClass = type.IsClass,
|
||||||
|
IsAbstract = type.IsAbstract,
|
||||||
|
BaseType = type.BaseType?.Name,
|
||||||
|
HasTestAttribute = ReflectionHelper.HasAttribute<TestAttribute>(type),
|
||||||
|
PropertyCount = ReflectionHelper.GetProperties<User>().Length,
|
||||||
|
IsValueType = ReflectionHelper.IsValueType(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(info.IsClass);
|
||||||
|
Assert.IsFalse(info.IsAbstract);
|
||||||
|
Assert.AreEqual("BaseEntity", info.BaseType);
|
||||||
|
Assert.IsTrue(info.HasTestAttribute);
|
||||||
|
Assert.IsTrue(info.PropertyCount >= 5);
|
||||||
|
Assert.IsFalse(info.IsValueType);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in new issue
Block a user