A:Common配置
This commit is contained in:
1 parent
61e769a0f6
commit
c8fde9759f
8 files changed
+3742
-703
No files matched your search
@@ -0,0 +1,530 @@
|
|||||||
|
namespace Common.ExpandFun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime 扩展方法
|
||||||
|
/// 提供日期格式化、时间戳转换、时区转换、日期计算等功能
|
||||||
|
/// </summary>
|
||||||
|
public static class DateTimeExpand
|
||||||
|
{
|
||||||
|
#region 日期格式化
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为中文日期字符串(yyyy年MM月dd日)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToChineseDateString(this DateTime dt)
|
||||||
|
{
|
||||||
|
return $"{dt.Year}年{dt.Month:D2}月{dt.Day:D2}日";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为中文日期时间字符串(yyyy年MM月dd日 HH:mm:ss)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToChineseDateTimeString(this DateTime dt)
|
||||||
|
{
|
||||||
|
return $"{dt.Year}年{dt.Month:D2}月{dt.Day:D2}日 {dt.Hour:D2}:{dt.Minute:D2}:{dt.Second:D2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为友好的相对时间字符串(如"刚刚"、"5分钟前"、"昨天"等)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToFriendlyString(this DateTime dt)
|
||||||
|
{
|
||||||
|
var span = DateTime.Now - dt;
|
||||||
|
|
||||||
|
if (span.TotalSeconds < 60)
|
||||||
|
return "刚刚";
|
||||||
|
|
||||||
|
if (span.TotalMinutes < 60)
|
||||||
|
return $"{(int)span.TotalMinutes}分钟前";
|
||||||
|
|
||||||
|
if (span.TotalHours < 24)
|
||||||
|
return $"{(int)span.TotalHours}小时前";
|
||||||
|
|
||||||
|
if (span.TotalDays < 2)
|
||||||
|
return "昨天";
|
||||||
|
|
||||||
|
if (span.TotalDays < 7)
|
||||||
|
return $"{(int)span.TotalDays}天前";
|
||||||
|
|
||||||
|
if (span.TotalDays < 30)
|
||||||
|
return $"{(int)(span.TotalDays / 7)}周前";
|
||||||
|
|
||||||
|
if (span.TotalDays < 365)
|
||||||
|
return $"{(int)(span.TotalDays / 30)}个月前";
|
||||||
|
|
||||||
|
return $"{(int)(span.TotalDays / 365)}年前";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为 ISO 8601 字符串(yyyy-MM-ddTHH:mm:ss.fffZ)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToIso8601String(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为标准日期字符串(yyyy-MM-dd)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToDateString(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.ToString("yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化为标准日期时间字符串(yyyy-MM-dd HH:mm:ss)
|
||||||
|
/// </summary>
|
||||||
|
public static string ToDateTimeString(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间戳转换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 Unix 时间戳(秒)
|
||||||
|
/// </summary>
|
||||||
|
public static long ToUnixTimeSeconds(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTimeOffset(dt).ToUnixTimeSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 Unix 时间戳(毫秒)
|
||||||
|
/// </summary>
|
||||||
|
public static long ToUnixTimeMilliseconds(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTimeOffset(dt).ToUnixTimeMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Unix 时间戳(秒)创建 DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime FromUnixTimeSeconds(long seconds)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(seconds).DateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Unix 时间戳(毫秒)创建 DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime FromUnixTimeMilliseconds(long milliseconds)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).DateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 JavaScript 时间戳(毫秒)
|
||||||
|
/// </summary>
|
||||||
|
public static long ToJavaScriptTimestamp(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.ToUnixTimeMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 JavaScript 时间戳(毫秒)创建 DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime FromJavaScriptTimestamp(long timestamp)
|
||||||
|
{
|
||||||
|
return FromUnixTimeMilliseconds(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时区转换
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为指定时区的时间
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dt">源时间</param>
|
||||||
|
/// <param name="targetTimeZoneId">目标时区 ID(如 "China Standard Time")</param>
|
||||||
|
/// <returns>转换后的时间</returns>
|
||||||
|
public static DateTime ConvertToTimeZone(this DateTime dt, string targetTimeZoneId)
|
||||||
|
{
|
||||||
|
var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZoneId);
|
||||||
|
return TimeZoneInfo.ConvertTime(dt, targetTimeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为 UTC 时间
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ToUtc(this DateTime dt)
|
||||||
|
{
|
||||||
|
if (dt.Kind == DateTimeKind.Utc)
|
||||||
|
return dt;
|
||||||
|
|
||||||
|
return TimeZoneInfo.ConvertTimeToUtc(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为本地时间
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ToLocal(this DateTime dt)
|
||||||
|
{
|
||||||
|
if (dt.Kind == DateTimeKind.Local)
|
||||||
|
return dt;
|
||||||
|
|
||||||
|
return dt.ToLocalTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 UTC 转换为指定时区
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime FromUtcToTimeZone(this DateTime utcDateTime, string timeZoneId)
|
||||||
|
{
|
||||||
|
if (utcDateTime.Kind != DateTimeKind.Utc)
|
||||||
|
throw new ArgumentException("DateTime must be UTC", nameof(utcDateTime));
|
||||||
|
|
||||||
|
var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
|
||||||
|
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, targetTimeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 日期计算
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前月份的第一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime StartOfMonth(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前月份的最后一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime EndOfMonth(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, dt.Month, DateTime.DaysInMonth(dt.Year, dt.Month), 23, 59, 59, 999, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前年份的第一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime StartOfYear(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前年份的最后一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime EndOfYear(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, 12, 31, 23, 59, 59, 999, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当天的开始时间(00:00:00)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime StartOfDay(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当天的结束时间(23:59:59.999)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime EndOfDay(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Date.AddDays(1).AddMilliseconds(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前周的第一天(周一)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime StartOfWeek(this DateTime dt, DayOfWeek firstDayOfWeek = DayOfWeek.Monday)
|
||||||
|
{
|
||||||
|
int diff = (7 + (dt.DayOfWeek - firstDayOfWeek)) % 7;
|
||||||
|
return dt.AddDays(-diff).Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前周的最后一天(周日)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime EndOfWeek(this DateTime dt, DayOfWeek firstDayOfWeek = DayOfWeek.Monday)
|
||||||
|
{
|
||||||
|
return dt.StartOfWeek(firstDayOfWeek).AddDays(6).EndOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加工作日(跳过周末)
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime AddWorkDays(this DateTime dt, int workDays)
|
||||||
|
{
|
||||||
|
int direction = workDays < 0 ? -1 : 1;
|
||||||
|
int daysToAdd = Math.Abs(workDays);
|
||||||
|
DateTime result = dt;
|
||||||
|
|
||||||
|
while (daysToAdd > 0)
|
||||||
|
{
|
||||||
|
result = result.AddDays(direction);
|
||||||
|
if (result.DayOfWeek != DayOfWeek.Saturday && result.DayOfWeek != DayOfWeek.Sunday)
|
||||||
|
{
|
||||||
|
daysToAdd--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算两个日期之间的工作日数量
|
||||||
|
/// </summary>
|
||||||
|
public static int WorkDaysBetween(this DateTime startDate, DateTime endDate)
|
||||||
|
{
|
||||||
|
if (startDate > endDate)
|
||||||
|
(startDate, endDate) = (endDate, startDate);
|
||||||
|
|
||||||
|
int workDays = 0;
|
||||||
|
for (DateTime date = startDate; date <= endDate; date = date.AddDays(1))
|
||||||
|
{
|
||||||
|
if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday)
|
||||||
|
{
|
||||||
|
workDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算年龄
|
||||||
|
/// </summary>
|
||||||
|
public static int CalculateAge(this DateTime birthDate, DateTime? referenceDate = null)
|
||||||
|
{
|
||||||
|
var reference = referenceDate ?? DateTime.Today;
|
||||||
|
int age = reference.Year - birthDate.Year;
|
||||||
|
|
||||||
|
if (reference.Month < birthDate.Month || (reference.Month == birthDate.Month && reference.Day < birthDate.Day))
|
||||||
|
{
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取季度(1-4)
|
||||||
|
/// </summary>
|
||||||
|
public static int GetQuarter(this DateTime dt)
|
||||||
|
{
|
||||||
|
return (dt.Month - 1) / 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取季度的第一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime StartOfQuarter(this DateTime dt)
|
||||||
|
{
|
||||||
|
int quarter = dt.GetQuarter();
|
||||||
|
int month = (quarter - 1) * 3 + 1;
|
||||||
|
return new DateTime(dt.Year, month, 1, 0, 0, 0, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取季度的最后一天
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime EndOfQuarter(this DateTime dt)
|
||||||
|
{
|
||||||
|
var startOfQuarter = dt.StartOfQuarter();
|
||||||
|
return startOfQuarter.AddMonths(3).AddDays(-1).EndOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间范围判断
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否在指定时间范围内
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsBetween(this DateTime dt, DateTime start, DateTime end)
|
||||||
|
{
|
||||||
|
return dt >= start && dt <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是今天
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsToday(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Date == DateTime.Today;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是昨天
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsYesterday(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Date == DateTime.Today.AddDays(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是明天
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsTomorrow(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Date == DateTime.Today.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是周末
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsWeekend(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.DayOfWeek == DayOfWeek.Saturday || dt.DayOfWeek == DayOfWeek.Sunday;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是工作日
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsWorkDay(this DateTime dt)
|
||||||
|
{
|
||||||
|
return !dt.IsWeekend();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是闰年
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsLeapYear(this DateTime dt)
|
||||||
|
{
|
||||||
|
return DateTime.IsLeapYear(dt.Year);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否在过去
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPast(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt < DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否在未来
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFuture(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt > DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是本周
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsThisWeek(this DateTime dt)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
return dt.StartOfWeek() == now.StartOfWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是本月
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsThisMonth(this DateTime dt)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
return dt.Year == now.Year && dt.Month == now.Month;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是本年
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsThisYear(this DateTime dt)
|
||||||
|
{
|
||||||
|
return dt.Year == DateTime.Now.Year;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间差计算
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取距离现在的友好时间差描述
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTimeSpanFromNow(this DateTime dt)
|
||||||
|
{
|
||||||
|
var span = DateTime.Now - dt;
|
||||||
|
return FormatTimeSpan(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取距离指定时间的友好时间差描述
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTimeSpanFrom(this DateTime dt, DateTime other)
|
||||||
|
{
|
||||||
|
var span = other - dt;
|
||||||
|
return FormatTimeSpan(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTimeSpan(TimeSpan span)
|
||||||
|
{
|
||||||
|
var abs = span.Duration();
|
||||||
|
|
||||||
|
if (abs.TotalDays >= 365)
|
||||||
|
return $"{(int)(abs.TotalDays / 365)}年";
|
||||||
|
|
||||||
|
if (abs.TotalDays >= 30)
|
||||||
|
return $"{(int)(abs.TotalDays / 30)}个月";
|
||||||
|
|
||||||
|
if (abs.TotalDays >= 1)
|
||||||
|
return $"{(int)abs.TotalDays}天";
|
||||||
|
|
||||||
|
if (abs.TotalHours >= 1)
|
||||||
|
return $"{(int)abs.TotalHours}小时";
|
||||||
|
|
||||||
|
if (abs.TotalMinutes >= 1)
|
||||||
|
return $"{(int)abs.TotalMinutes}分钟";
|
||||||
|
|
||||||
|
return $"{(int)abs.TotalSeconds}秒";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间舍入
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 舍入到最近的小时
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime RoundToHour(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 舍入到最近的分钟
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime RoundToMinute(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 舍入到最近的秒
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime RoundToSecond(this DateTime dt)
|
||||||
|
{
|
||||||
|
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向上舍入到指定分钟间隔
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime CeilingToMinutes(this DateTime dt, int minutes)
|
||||||
|
{
|
||||||
|
var ticks = TimeSpan.FromMinutes(minutes).Ticks;
|
||||||
|
return new DateTime((dt.Ticks + ticks - 1) / ticks * ticks, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向下舍入到指定分钟间隔
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime FloorToMinutes(this DateTime dt, int minutes)
|
||||||
|
{
|
||||||
|
var ticks = TimeSpan.FromMinutes(minutes).Ticks;
|
||||||
|
return new DateTime(dt.Ticks / ticks * ticks, dt.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
namespace Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Global 轻量级 DI 容器使用示例
|
|
||||||
/// </summary>
|
|
||||||
public static class GlobalUsageExamples
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 基础使用示例
|
|
||||||
/// </summary>
|
|
||||||
public static void BasicUsageExamples()
|
|
||||||
{
|
|
||||||
// 1. 注册和获取单例服务
|
|
||||||
var config = new AppConfig { AppName = "MyApp", Version = "1.0" };
|
|
||||||
Global.RegisterSingleton(config);
|
|
||||||
|
|
||||||
var retrievedConfig = Global.Get<AppConfig>();
|
|
||||||
Console.WriteLine($"App: {retrievedConfig.AppName} v{retrievedConfig.Version}");
|
|
||||||
|
|
||||||
// 2. 使用工厂方法注册单例
|
|
||||||
Global.RegisterSingleton(() => new DatabaseConnection("connection-string"));
|
|
||||||
var db = Global.Get<DatabaseConnection>();
|
|
||||||
|
|
||||||
// 3. 注册瞬态服务(每次获取都是新实例)
|
|
||||||
Global.RegisterTransient(() => new RequestContext
|
|
||||||
{
|
|
||||||
RequestId = Guid.NewGuid().ToString(),
|
|
||||||
Timestamp = DateTime.Now
|
|
||||||
});
|
|
||||||
|
|
||||||
var request1 = Global.Get<RequestContext>();
|
|
||||||
var request2 = Global.Get<RequestContext>();
|
|
||||||
// request1 和 request2 是不同的实例
|
|
||||||
|
|
||||||
// 4. 尝试获取服务
|
|
||||||
if (Global.TryGet<ILogger>(out var logger))
|
|
||||||
{
|
|
||||||
logger.Log("Service found");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("Logger not registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 获取服务或默认值
|
|
||||||
var fallbackLogger = Global.GetOrDefault<ILogger>(new ConsoleLogger());
|
|
||||||
|
|
||||||
// 6. 检查服务是否注册
|
|
||||||
if (Global.IsRegistered<AppConfig>())
|
|
||||||
{
|
|
||||||
Console.WriteLine("AppConfig is registered");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务变更通知示例
|
|
||||||
/// </summary>
|
|
||||||
public static void ChangeNotificationExamples()
|
|
||||||
{
|
|
||||||
// 1. 订阅服务变更通知
|
|
||||||
Global.Subscribe<AppConfig>(eventArgs =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Config changed at {eventArgs.ChangedAt}");
|
|
||||||
Console.WriteLine($"Old: {eventArgs.OldValue?.AppName ?? "null"}");
|
|
||||||
Console.WriteLine($"New: {eventArgs.NewValue.AppName}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 注册初始配置
|
|
||||||
var config = new AppConfig { AppName = "MyApp", Version = "1.0" };
|
|
||||||
Global.RegisterSingleton(config);
|
|
||||||
|
|
||||||
// 3. 更新配置(会触发通知)
|
|
||||||
var newConfig = new AppConfig { AppName = "MyApp", Version = "2.0" };
|
|
||||||
Global.Update(newConfig);
|
|
||||||
|
|
||||||
// 4. 也可以在注册时就设置为通知模式
|
|
||||||
var anotherConfig = new AppConfig { AppName = "MyApp", Version = "3.0" };
|
|
||||||
Global.RegisterSingleton(anotherConfig, notifyIfExists: true); // 会触发通知
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 实时配置更新示例
|
|
||||||
/// </summary>
|
|
||||||
public static void LiveConfigurationExample()
|
|
||||||
{
|
|
||||||
// 场景:应用程序配置可以动态更新,所有组件都能收到通知
|
|
||||||
|
|
||||||
// 1. 定义配置变更处理器
|
|
||||||
void OnConfigChanged(ServiceChangedEventArgs<AppConfig> e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Configuration updated:");
|
|
||||||
Console.WriteLine($" Old Version: {e.OldValue?.Version}");
|
|
||||||
Console.WriteLine($" New Version: {e.NewValue.Version}");
|
|
||||||
|
|
||||||
// 在这里可以执行配置变更后的操作
|
|
||||||
// 例如:重新连接数据库、刷新缓存等
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 订阅配置变更
|
|
||||||
Global.Subscribe<AppConfig>(OnConfigChanged);
|
|
||||||
|
|
||||||
// 3. 注册初始配置
|
|
||||||
Global.RegisterSingleton(new AppConfig { AppName = "MyApp", Version = "1.0" });
|
|
||||||
|
|
||||||
// 4. 模拟配置文件被修改,更新配置
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
Global.Update(new AppConfig { AppName = "MyApp", Version = "1.1" });
|
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
Global.Update(new AppConfig { AppName = "MyApp", Version = "1.2" });
|
|
||||||
|
|
||||||
// 5. 取消订阅
|
|
||||||
Global.Unsubscribe<AppConfig>(OnConfigChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 多订阅者示例
|
|
||||||
/// </summary>
|
|
||||||
public static void MultipleSubscribersExample()
|
|
||||||
{
|
|
||||||
// 场景:多个组件都关心某个服务的变更
|
|
||||||
|
|
||||||
// 订阅者 1:UI 组件
|
|
||||||
Global.Subscribe<UserSession>(e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[UI] User session updated: {e.NewValue.Username}");
|
|
||||||
// 更新UI显示
|
|
||||||
});
|
|
||||||
|
|
||||||
// 订阅者 2:日志组件
|
|
||||||
Global.Subscribe<UserSession>(e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[LOG] Session change detected at {e.ChangedAt}");
|
|
||||||
// 记录日志
|
|
||||||
});
|
|
||||||
|
|
||||||
// 订阅者 3:分析组件
|
|
||||||
Global.Subscribe<UserSession>(e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[ANALYTICS] Tracking user: {e.NewValue.Username}");
|
|
||||||
// 发送分析数据
|
|
||||||
});
|
|
||||||
|
|
||||||
// 注册会话
|
|
||||||
Global.RegisterSingleton(new UserSession { Username = "user1", LoginTime = DateTime.Now });
|
|
||||||
|
|
||||||
// 更新会话(所有订阅者都会收到通知)
|
|
||||||
Thread.Sleep(100);
|
|
||||||
Global.Update(new UserSession { Username = "user2", LoginTime = DateTime.Now });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务生命周期对比示例
|
|
||||||
/// </summary>
|
|
||||||
public static void LifetimeComparisonExample()
|
|
||||||
{
|
|
||||||
// 1. 单例服务:整个应用生命周期内只有一个实例
|
|
||||||
Global.RegisterSingleton(() => new CacheService());
|
|
||||||
var cache1 = Global.Get<CacheService>();
|
|
||||||
var cache2 = Global.Get<CacheService>();
|
|
||||||
Console.WriteLine($"Singleton: {ReferenceEquals(cache1, cache2)}"); // True
|
|
||||||
|
|
||||||
// 2. 瞬态服务:每次获取都是新实例
|
|
||||||
Global.RegisterTransient(() => new OperationContext());
|
|
||||||
var ctx1 = Global.Get<OperationContext>();
|
|
||||||
var ctx2 = Global.Get<OperationContext>();
|
|
||||||
Console.WriteLine($"Transient: {ReferenceEquals(ctx1, ctx2)}"); // False
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务管理示例
|
|
||||||
/// </summary>
|
|
||||||
public static void ServiceManagementExample()
|
|
||||||
{
|
|
||||||
// 注册多个服务
|
|
||||||
Global.RegisterSingleton(new AppConfig { AppName = "MyApp" });
|
|
||||||
Global.RegisterSingleton(() => new DatabaseConnection("conn-string"));
|
|
||||||
Global.RegisterTransient(() => new RequestContext());
|
|
||||||
|
|
||||||
// 查看已注册服务数量
|
|
||||||
Console.WriteLine($"Total services: {Global.Count}");
|
|
||||||
|
|
||||||
// 注销特定服务
|
|
||||||
if (Global.Unregister<RequestContext>())
|
|
||||||
{
|
|
||||||
Console.WriteLine("RequestContext unregistered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除所有服务
|
|
||||||
Global.Clear();
|
|
||||||
Console.WriteLine($"Services after clear: {Global.Count}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 错误处理示例
|
|
||||||
/// </summary>
|
|
||||||
public static void ErrorHandlingExample()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 尝试获取未注册的服务
|
|
||||||
var service = Global.Get<ILogger>();
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 尝试更新瞬态服务
|
|
||||||
Global.RegisterTransient(() => new RequestContext());
|
|
||||||
Global.Update(new RequestContext()); // 会抛出异常
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安全的获取方式
|
|
||||||
if (Global.TryGet<ILogger>(out var logger))
|
|
||||||
{
|
|
||||||
logger.Log("Success");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("Logger not available, using fallback");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 工厂模式集成示例
|
|
||||||
/// </summary>
|
|
||||||
public static void FactoryPatternExample()
|
|
||||||
{
|
|
||||||
// 使用工厂方法创建复杂对象
|
|
||||||
Global.RegisterSingleton(() =>
|
|
||||||
{
|
|
||||||
var config = LoadConfigFromFile();
|
|
||||||
ValidateConfig(config);
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 延迟初始化:只有在第一次 Get 时才会调用工厂方法
|
|
||||||
var config = Global.Get<AppConfig>(); // 此时才执行工厂方法
|
|
||||||
|
|
||||||
// 后续获取都返回同一个实例
|
|
||||||
var sameConfig = Global.Get<AppConfig>(); // 直接返回,不再调用工厂
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 线程安全示例
|
|
||||||
/// </summary>
|
|
||||||
public static void ThreadSafetyExample()
|
|
||||||
{
|
|
||||||
// 注册服务
|
|
||||||
Global.RegisterSingleton(() =>
|
|
||||||
{
|
|
||||||
Thread.Sleep(100); // 模拟复杂的初始化过程
|
|
||||||
return new CacheService();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 多线程并发获取(保证只创建一次)
|
|
||||||
var tasks = Enumerable.Range(0, 10).Select(i =>
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
var service = Global.Get<CacheService>();
|
|
||||||
Console.WriteLine($"Thread {i}: {service.GetHashCode()}");
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
Task.WaitAll(tasks.ToArray());
|
|
||||||
// 所有线程获取的都是同一个实例
|
|
||||||
}
|
|
||||||
|
|
||||||
#region 辅助类型
|
|
||||||
|
|
||||||
private class AppConfig
|
|
||||||
{
|
|
||||||
public string AppName { get; set; } = "";
|
|
||||||
public string Version { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DatabaseConnection
|
|
||||||
{
|
|
||||||
public string ConnectionString { get; }
|
|
||||||
public DatabaseConnection(string connectionString)
|
|
||||||
{
|
|
||||||
ConnectionString = connectionString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RequestContext
|
|
||||||
{
|
|
||||||
public string RequestId { get; set; } = Guid.NewGuid().ToString();
|
|
||||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface ILogger
|
|
||||||
{
|
|
||||||
void Log(string message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConsoleLogger : ILogger
|
|
||||||
{
|
|
||||||
public void Log(string message) => Console.WriteLine($"[LOG] {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UserSession
|
|
||||||
{
|
|
||||||
public string Username { get; set; } = "";
|
|
||||||
public DateTime LoginTime { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CacheService
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, object> _cache = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class OperationContext
|
|
||||||
{
|
|
||||||
public Guid OperationId { get; } = Guid.NewGuid();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AppConfig LoadConfigFromFile()
|
|
||||||
{
|
|
||||||
// 模拟从文件加载配置
|
|
||||||
return new AppConfig { AppName = "MyApp", Version = "1.0" };
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateConfig(AppConfig config)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(config.AppName))
|
|
||||||
throw new InvalidOperationException("AppName cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
# Sharp.Common
|
||||||
|
|
||||||
|
一个功能丰富的 C# 通用工具库,提供依赖注入、函数式错误处理、字符串扩展、JSON 序列化等常用功能。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- ✅ **轻量级 DI 容器** - 线程安全的依赖注入容器,支持单例和瞬态生命周期,带变更通知
|
||||||
|
- ✅ **Result 模式** - 函数式错误处理,避免异常,支持 LINQ 和异步操作
|
||||||
|
- ✅ **自定义异常** - 带错误码和建议的异常类型
|
||||||
|
- ✅ **字符串扩展** - URL 验证、安全类型转换等实用方法
|
||||||
|
- ✅ **日期时间扩展** - 日期格式化、时间戳转换、时区转换、工作日计算
|
||||||
|
- ✅ **加密工具** - AES/RSA加密、MD5/SHA256哈希、Base64编解码、密码哈希、HMAC签名
|
||||||
|
- ✅ **验证工具** - 邮箱、手机号、身份证、URL验证,与Result模式无缝集成
|
||||||
|
- ✅ **JSON 序列化** - 预配置的 JSON 序列化,支持 camelCase 和 IPAddress
|
||||||
|
- ✅ **系统工具** - 动态端口分配、管理员权限检查
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
将 `Common` 项目引用添加到你的项目中:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="path\to\Sharp\Common\Common.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基础使用
|
||||||
|
|
||||||
|
#### 1. 依赖注入容器 (Global)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common;
|
||||||
|
|
||||||
|
// 注册单例服务
|
||||||
|
var config = new AppConfig { AppName = "MyApp" };
|
||||||
|
Global.RegisterSingleton(config);
|
||||||
|
|
||||||
|
// 获取服务
|
||||||
|
var retrievedConfig = Global.Get<AppConfig>();
|
||||||
|
|
||||||
|
// 订阅变更通知
|
||||||
|
Global.Subscribe<AppConfig>(e =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Config changed: {e.OldValue?.AppName} -> {e.NewValue.AppName}");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新服务(触发通知)
|
||||||
|
Global.Update(new AppConfig { AppName = "UpdatedApp" });
|
||||||
|
|
||||||
|
// 注册瞬态服务(每次获取都创建新实例)
|
||||||
|
Global.RegisterTransient(() => new RequestContext());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Result 模式
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Results;
|
||||||
|
|
||||||
|
// 创建成功结果
|
||||||
|
var success = Result.Success(42);
|
||||||
|
|
||||||
|
// 创建失败结果
|
||||||
|
var failure = Result.Failure<int>("Something went wrong");
|
||||||
|
var failureWithCode = Result.Failure<int>(404L, "Not found");
|
||||||
|
|
||||||
|
// 使用 Try 安全执行操作
|
||||||
|
var result = Result.Try(() => int.Parse("123"));
|
||||||
|
|
||||||
|
// 函数式链式操作
|
||||||
|
var processedResult = Result.Success(10)
|
||||||
|
.Map(x => x * 2) // 转换值
|
||||||
|
.Where(x => x > 15, "Too small") // 过滤
|
||||||
|
.Bind(x => Result.Success(x.ToString()));
|
||||||
|
|
||||||
|
// 条件执行
|
||||||
|
result
|
||||||
|
.OnSuccess(value => Console.WriteLine($"Success: {value}"))
|
||||||
|
.OnFailure((ex, msg) => Console.WriteLine($"Error: {msg}"));
|
||||||
|
|
||||||
|
// LINQ 查询语法
|
||||||
|
var combined = from x in Result.Success(5)
|
||||||
|
from y in Result.Success(10)
|
||||||
|
select x + y;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 字符串扩展
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
// URL 验证
|
||||||
|
bool isValid = "https://example.com".IsValidUrl(); // true
|
||||||
|
|
||||||
|
// 安全类型转换
|
||||||
|
int port = "8080".ToInt(80); // 8080
|
||||||
|
int invalid = "abc".ToInt(80); // 80 (使用默认值)
|
||||||
|
int? nullable = "123".TryToInt(); // 123
|
||||||
|
|
||||||
|
double price = "99.99".ToDouble(); // 99.99
|
||||||
|
double? weight = "invalid".TryToDouble(); // null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. JSON 序列化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
// 序列化(使用 camelCase)
|
||||||
|
var person = new Person { Name = "John", Age = 30 };
|
||||||
|
var json = JsonHelper.ToJson(person);
|
||||||
|
// {"name": "John", "age": 30}
|
||||||
|
|
||||||
|
// 反序列化
|
||||||
|
var loaded = JsonHelper.FromJson<Person>(json);
|
||||||
|
|
||||||
|
// 支持 IPAddress
|
||||||
|
var networkInfo = new NetworkInfo
|
||||||
|
{
|
||||||
|
ServerIp = IPAddress.Parse("192.168.1.1")
|
||||||
|
};
|
||||||
|
var jsonWithIp = JsonHelper.ToJson(networkInfo);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 系统工具
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
// 获取可用端口
|
||||||
|
int port = SystemHelper.GetAvailablePort();
|
||||||
|
Console.WriteLine($"Server listening on port {port}");
|
||||||
|
|
||||||
|
// 检查管理员权限(仅 Windows)
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
bool isAdmin = SystemHelper.IsAdministrator();
|
||||||
|
if (!isAdmin)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Please run as administrator");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. 加密工具
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
// AES 加密/解密
|
||||||
|
var encrypted = CryptoHelper.AesEncrypt("敏感数据", "MyPassword123");
|
||||||
|
var decrypted = CryptoHelper.AesDecrypt(encrypted, "MyPassword123");
|
||||||
|
|
||||||
|
// RSA 公钥加密
|
||||||
|
var (publicKey, privateKey) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
var rsaEncrypted = CryptoHelper.RsaEncrypt("Secret", publicKey);
|
||||||
|
var rsaDecrypted = CryptoHelper.RsaDecrypt(rsaEncrypted, privateKey);
|
||||||
|
|
||||||
|
// 哈希
|
||||||
|
var md5 = CryptoHelper.Md5Hash("data");
|
||||||
|
var sha256 = CryptoHelper.Sha256Hash("data");
|
||||||
|
|
||||||
|
// 密码哈希和验证(PBKDF2)
|
||||||
|
var hashedPassword = CryptoHelper.HashPassword("MySecurePassword");
|
||||||
|
bool isValid = CryptoHelper.VerifyPassword("MySecurePassword", hashedPassword);
|
||||||
|
|
||||||
|
// HMAC 签名
|
||||||
|
var signature = CryptoHelper.HmacSha256("message", "secret");
|
||||||
|
bool verified = CryptoHelper.VerifyHmacSha256("message", signature, "secret");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. 日期时间扩展
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
var date = DateTime.Now;
|
||||||
|
|
||||||
|
// 日期格式化
|
||||||
|
var chinese = date.ToChineseDateString(); // "2024年01月15日"
|
||||||
|
var friendly = date.ToFriendlyString(); // "5分钟前"
|
||||||
|
var iso = date.ToIso8601String(); // "2024-01-15T12:30:45.123Z"
|
||||||
|
|
||||||
|
// 时间戳转换
|
||||||
|
long timestamp = date.ToUnixTimeSeconds();
|
||||||
|
var fromTimestamp = DateTimeExpand.FromUnixTimeSeconds(timestamp);
|
||||||
|
|
||||||
|
// 日期计算
|
||||||
|
var monthStart = date.StartOfMonth();
|
||||||
|
var monthEnd = date.EndOfMonth();
|
||||||
|
var nextWorkDay = date.AddWorkDays(5);
|
||||||
|
int workDays = date.WorkDaysBetween(date.AddDays(10));
|
||||||
|
int age = birthDate.CalculateAge();
|
||||||
|
|
||||||
|
// 时间范围判断
|
||||||
|
bool isToday = date.IsToday();
|
||||||
|
bool isWeekend = date.IsWeekend();
|
||||||
|
bool inRange = date.IsBetween(start, end);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. 验证工具
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.Utils;
|
||||||
|
using Common.Results;
|
||||||
|
|
||||||
|
// 基础验证
|
||||||
|
bool isValidEmail = ValidationHelper.IsValidEmail("user@example.com");
|
||||||
|
bool isValidMobile = ValidationHelper.IsValidChinaMobile("13812345678");
|
||||||
|
bool isValidIdCard = ValidationHelper.IsValidChinaIdCard("110105199001150011");
|
||||||
|
|
||||||
|
// Result 模式集成
|
||||||
|
var emailResult = ValidationHelper.ValidateEmail("user@example.com");
|
||||||
|
if (emailResult.IsSuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Valid email: {emailResult.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码强度验证
|
||||||
|
var passwordResult = ValidationHelper.ValidatePassword(
|
||||||
|
"MyPass123!",
|
||||||
|
minLength: 8,
|
||||||
|
requireDigit: true,
|
||||||
|
requireUppercase: true,
|
||||||
|
requireSpecialChar: true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 组合验证
|
||||||
|
var result = ValidationHelper.ValidateAll(
|
||||||
|
userInput,
|
||||||
|
i => ValidationHelper.ValidateNotEmpty(i, "用户名"),
|
||||||
|
i => ValidationHelper.ValidateLength(i, 3, 20, "用户名"),
|
||||||
|
i => ValidationHelper.ValidateRegex(i, "^[a-zA-Z0-9_]+$", "用户名只能包含字母、数字和下划线")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 从身份证提取信息
|
||||||
|
var birthDate = ValidationHelper.ExtractBirthDateFromIdCard("110105199001150011");
|
||||||
|
var gender = ValidationHelper.ExtractGenderFromIdCard("110105199001150011");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 9. 自定义异常
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Common.ExpandException;
|
||||||
|
|
||||||
|
// 创建带错误码和建议的异常
|
||||||
|
throw new ExceptionExpand(
|
||||||
|
404L,
|
||||||
|
"Resource not found",
|
||||||
|
"Check the resource ID and try again"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用预定义异常
|
||||||
|
throw ExampleExceptionEnum.ProcessGetException;
|
||||||
|
|
||||||
|
// 在 Result 中使用
|
||||||
|
var exception = new ExceptionExpand(1001L, "Business error", "Check input");
|
||||||
|
var result = Result.Failure<string>(exception);
|
||||||
|
Console.WriteLine(result.ErrorCode); // 1001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 详细文档
|
||||||
|
|
||||||
|
### Global - 依赖注入容器
|
||||||
|
|
||||||
|
#### 注册服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 注册单例(直接实例)
|
||||||
|
Global.RegisterSingleton(new AppConfig());
|
||||||
|
|
||||||
|
// 注册单例(工厂方法,延迟初始化)
|
||||||
|
Global.RegisterSingleton(() => new DatabaseConnection("connection-string"));
|
||||||
|
|
||||||
|
// 注册瞬态服务
|
||||||
|
Global.RegisterTransient(() => new RequestContext { Id = Guid.NewGuid() });
|
||||||
|
|
||||||
|
// 通用注册方法
|
||||||
|
Global.Register(() => new CacheService(), ServiceLifetime.Singleton);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 直接获取(不存在会抛异常)
|
||||||
|
var config = Global.Get<AppConfig>();
|
||||||
|
|
||||||
|
// 安全获取
|
||||||
|
if (Global.TryGet<ILogger>(out var logger))
|
||||||
|
{
|
||||||
|
logger.Log("Found logger");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或使用默认值
|
||||||
|
var logger = Global.GetOrDefault<ILogger>(new ConsoleLogger());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 变更通知
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 订阅变更
|
||||||
|
Global.Subscribe<AppConfig>(e =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Changed at: {e.ChangedAt}");
|
||||||
|
Console.WriteLine($"Old: {e.OldValue?.AppName}");
|
||||||
|
Console.WriteLine($"New: {e.NewValue.AppName}");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新服务(触发通知)
|
||||||
|
Global.Update(new AppConfig { AppName = "New Version" });
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
Global.Unsubscribe<AppConfig>(handler);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 检查服务是否注册
|
||||||
|
bool exists = Global.IsRegistered<AppConfig>();
|
||||||
|
|
||||||
|
// 注销服务
|
||||||
|
Global.Unregister<AppConfig>();
|
||||||
|
|
||||||
|
// 清除所有服务
|
||||||
|
Global.Clear();
|
||||||
|
|
||||||
|
// 获取服务数量
|
||||||
|
int count = Global.Count;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Result - 函数式错误处理
|
||||||
|
|
||||||
|
#### 创建 Result
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 成功
|
||||||
|
var success = Result.Success(); // 无返回值
|
||||||
|
var successWithValue = Result.Success(42);
|
||||||
|
|
||||||
|
// 失败
|
||||||
|
var failure1 = Result.Failure<int>("Error message");
|
||||||
|
var failure2 = Result.Failure<int>(new Exception("Error"));
|
||||||
|
var failure3 = Result.Failure<int>(404L, "Not found");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Try 模式
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 同步
|
||||||
|
var result = Result.Try(() => int.Parse("123"));
|
||||||
|
|
||||||
|
// 异步
|
||||||
|
var asyncResult = await Result.TryAsync(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
return 42;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 函数式操作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Map: 转换成功值
|
||||||
|
var doubled = Result.Success(10).Map(x => x * 2);
|
||||||
|
|
||||||
|
// Bind: 链式操作(返回 Result)
|
||||||
|
var chained = Result.Success(10)
|
||||||
|
.Bind(x => x > 5 ? Result.Success(x * 2) : Result.Failure<int>("Too small"));
|
||||||
|
|
||||||
|
// Where/Ensure: 过滤和验证
|
||||||
|
var filtered = Result.Success(10)
|
||||||
|
.Where(x => x > 5, "Value must be > 5")
|
||||||
|
.Ensure(x => x < 100, "Value must be < 100");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 值提取
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = Result.Success(42);
|
||||||
|
|
||||||
|
// 不同的提取方式
|
||||||
|
int? nullable = result.GetOrNullable(); // 42
|
||||||
|
int withDefault = result.GetOrDefault(0); // 42
|
||||||
|
int orThrow = result.GetOrThrow(); // 42 或抛出异常
|
||||||
|
int orElse = result.GetOrElse(() => 0); // 42 或执行函数
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 条件执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
result
|
||||||
|
.OnSuccess(value => Console.WriteLine($"Success: {value}"))
|
||||||
|
.OnFailure((ex, msg) => Console.WriteLine($"Error: {msg}"));
|
||||||
|
|
||||||
|
// Match 模式
|
||||||
|
var message = result.Match(
|
||||||
|
onSuccess: value => $"Success: {value}",
|
||||||
|
onFailure: (ex, msg) => $"Error: {msg}"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LINQ 支持
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = from x in Result.Success(5)
|
||||||
|
from y in Result.Success(10)
|
||||||
|
where x > 0 && y > 0
|
||||||
|
select x + y;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误码支持
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = Result.Failure<int>(404L, "Not found");
|
||||||
|
|
||||||
|
// 检查错误码
|
||||||
|
if (result.HasErrorCode(404L))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Resource not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取错误码
|
||||||
|
long code = result.GetErrorCodeOrDefault(-1);
|
||||||
|
|
||||||
|
// 从 ExceptionExpand 创建
|
||||||
|
var exception = new ExceptionExpand(1001L, "Error", "Suggestion");
|
||||||
|
var fromException = ResultExtensions.FromExceptionExpand<int>(exception);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展方法
|
||||||
|
|
||||||
|
#### StringExpand
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// URL 验证
|
||||||
|
"https://example.com".IsValidUrl(); // true
|
||||||
|
"not-a-url".IsValidUrl(); // false
|
||||||
|
|
||||||
|
// 整数转换
|
||||||
|
"123".ToInt(); // 123
|
||||||
|
"abc".ToInt(999); // 999 (默认值)
|
||||||
|
"456".TryToInt(); // 456?
|
||||||
|
|
||||||
|
// 浮点数转换
|
||||||
|
"3.14".ToDouble(); // 3.14
|
||||||
|
"abc".ToDouble(0.0); // 0.0 (默认值)
|
||||||
|
"2.5".TryToDouble(); // 2.5?
|
||||||
|
```
|
||||||
|
|
||||||
|
### JsonHelper
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 序列化
|
||||||
|
var json = JsonHelper.ToJson(myObject);
|
||||||
|
|
||||||
|
// 反序列化
|
||||||
|
var obj = JsonHelper.FromJson<MyType>(json);
|
||||||
|
|
||||||
|
// 特性
|
||||||
|
// - 使用 camelCase 命名
|
||||||
|
// - 格式化输出(缩进)
|
||||||
|
// - 支持枚举转字符串
|
||||||
|
// - 支持 IPAddress 类型
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemHelper
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 获取可用端口(用于动态端口绑定)
|
||||||
|
int port = SystemHelper.GetAvailablePort();
|
||||||
|
|
||||||
|
// 检查管理员权限(仅 Windows)
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
bool isAdmin = SystemHelper.IsAdministrator();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实际应用场景
|
||||||
|
|
||||||
|
### 场景 1:配置热更新
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 订阅配置变更
|
||||||
|
Global.Subscribe<AppConfig>(e =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Configuration updated: v{e.OldValue?.Version} -> v{e.NewValue.Version}");
|
||||||
|
// 重新初始化依赖配置的组件
|
||||||
|
RefreshComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册初始配置
|
||||||
|
Global.RegisterSingleton(LoadConfiguration());
|
||||||
|
|
||||||
|
// 文件监控器检测到配置文件变更
|
||||||
|
void OnConfigFileChanged()
|
||||||
|
{
|
||||||
|
var newConfig = LoadConfiguration();
|
||||||
|
Global.Update(newConfig); // 自动通知所有订阅者
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:数据验证流水线
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<User> ValidateAndCreateUser(string email, string password, int age)
|
||||||
|
{
|
||||||
|
return email.AsResult()
|
||||||
|
.Ensure(e => !string.IsNullOrEmpty(e), 400L, "Email is required")
|
||||||
|
.Ensure(e => e.Contains("@"), 400L, "Invalid email format")
|
||||||
|
.Bind(e => ValidatePassword(password)
|
||||||
|
.Map(_ => e))
|
||||||
|
.Bind(e => ValidateAge(age)
|
||||||
|
.Map(_ => e))
|
||||||
|
.Map(e => new User { Email = e, Age = age });
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<string> ValidatePassword(string password)
|
||||||
|
{
|
||||||
|
return password.AsResult()
|
||||||
|
.Ensure(p => p.Length >= 8, 400L, "Password must be at least 8 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<int> ValidateAge(int age)
|
||||||
|
{
|
||||||
|
return age.AsResult()
|
||||||
|
.Ensure(a => a >= 18, 400L, "User must be at least 18 years old")
|
||||||
|
.Ensure(a => a <= 120, 400L, "Invalid age");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:集成测试动态端口
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[TestClass]
|
||||||
|
public class IntegrationTests
|
||||||
|
{
|
||||||
|
private TestServer _server;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// 动态分配端口,避免端口冲突
|
||||||
|
var port = SystemHelper.GetAvailablePort();
|
||||||
|
_server = new TestServer(port);
|
||||||
|
_server.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_server?.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task TestApiEndpoint()
|
||||||
|
{
|
||||||
|
var client = new HttpClient();
|
||||||
|
var response = await client.GetAsync($"http://localhost:{_server.Port}/api/health");
|
||||||
|
Assert.IsTrue(response.IsSuccessStatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4:API 错误处理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<Result<User>> GetUserAsync(int userId)
|
||||||
|
{
|
||||||
|
return await Result.TryAsync(async () =>
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync($"/api/users/{userId}");
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return response.StatusCode switch
|
||||||
|
{
|
||||||
|
HttpStatusCode.NotFound => Result.Failure<User>(404L, "User not found"),
|
||||||
|
HttpStatusCode.Unauthorized => Result.Failure<User>(401L, "Unauthorized"),
|
||||||
|
_ => Result.Failure<User>(500L, "Server error")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var user = JsonHelper.FromJson<User>(json);
|
||||||
|
|
||||||
|
return user != null
|
||||||
|
? Result.Success(user)
|
||||||
|
: Result.Failure<User>("Failed to parse user data");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
var result = await GetUserAsync(123);
|
||||||
|
result
|
||||||
|
.OnSuccess(user => Console.WriteLine($"Found user: {user.Name}"))
|
||||||
|
.OnFailure((ex, msg, code) =>
|
||||||
|
{
|
||||||
|
if (code == 404L)
|
||||||
|
Console.WriteLine("User not found, creating new user...");
|
||||||
|
else
|
||||||
|
Console.WriteLine($"Error {code}: {msg}");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
项目包含完整的单元测试,位于 `test/Common.Test` 目录:
|
||||||
|
|
||||||
|
- `GlobalTests.cs` - DI 容器测试(包含线程安全测试)
|
||||||
|
- `ResultTests.cs` - Result 模式测试(包含 LINQ 和异步测试)
|
||||||
|
- `ExceptionExpandTests.cs` - 自定义异常测试
|
||||||
|
- `StringExpandTests.cs` - 字符串扩展测试
|
||||||
|
- `JsonHelperTests.cs` - JSON 序列化测试
|
||||||
|
- `SystemHelperTests.cs` - 系统工具测试
|
||||||
|
|
||||||
|
运行测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test/Common.Test
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### DI 容器
|
||||||
|
|
||||||
|
1. **生命周期选择**
|
||||||
|
- 使用 **Singleton** 用于:配置对象、数据库连接池、缓存服务
|
||||||
|
- 使用 **Transient** 用于:请求上下文、操作上下文
|
||||||
|
|
||||||
|
2. **变更通知**
|
||||||
|
- 订阅者应该是轻量级的,避免长时间阻塞
|
||||||
|
- 订阅者中不要抛出异常(已被容器捕获和忽略)
|
||||||
|
|
||||||
|
3. **线程安全**
|
||||||
|
- 容器本身是线程安全的
|
||||||
|
- 单例服务的初始化是线程安全的(双重检查锁定)
|
||||||
|
|
||||||
|
### Result 模式
|
||||||
|
|
||||||
|
1. **何时使用 Result**
|
||||||
|
- 可预期的业务错误(验证失败、资源不存在)
|
||||||
|
- 外部 API 调用
|
||||||
|
- 需要传递错误码的场景
|
||||||
|
|
||||||
|
2. **何时使用异常**
|
||||||
|
- 不可预期的程序错误(NullReferenceException、OutOfMemoryException)
|
||||||
|
- 系统级错误
|
||||||
|
|
||||||
|
3. **链式操作**
|
||||||
|
- 使用 `Map` 转换成功值
|
||||||
|
- 使用 `Bind` 链接返回 Result 的操作
|
||||||
|
- 使用 `Ensure` 进行验证
|
||||||
|
- 失败会自动传播,不需要手动检查
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Sharp/Common/
|
||||||
|
├── Global.cs # DI 容器
|
||||||
|
├── ExpandException/
|
||||||
|
│ ├── ExceptionExpand.cs # 自定义异常
|
||||||
|
│ └── ExampleExceptionEnum.cs # 预定义异常
|
||||||
|
├── ExpandFun/
|
||||||
|
│ └── StringExpand.cs # 字符串扩展
|
||||||
|
├── Results/
|
||||||
|
│ ├── Result.cs # Result 核心类
|
||||||
|
│ └── ResultExtensions.cs # Result 扩展方法
|
||||||
|
└── Utils/
|
||||||
|
├── JsonHelper.cs # JSON 工具
|
||||||
|
└── SystemHelper.cs # 系统工具
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可
|
||||||
|
|
||||||
|
本项目为内部库,供团队项目使用。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request。
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- ✅ 轻量级 DI 容器(支持生命周期和变更通知)
|
||||||
|
- ✅ Result 模式(完整的函数式错误处理)
|
||||||
|
- ✅ 自定义异常(带错误码)
|
||||||
|
- ✅ 字符串扩展(URL 验证、类型转换)
|
||||||
|
- ✅ JSON 序列化(camelCase、IPAddress 支持)
|
||||||
|
- ✅ 系统工具(动态端口、管理员检查)
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
using Common.ExpandException;
|
|
||||||
|
|
||||||
namespace Common.Results;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Examples demonstrating how to use the Result《T》 class and its extensions
|
|
||||||
/// </summary>
|
|
||||||
public static class ResultUsageExamples
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Basic usage examples
|
|
||||||
/// </summary>
|
|
||||||
public static void BasicUsageExamples()
|
|
||||||
{
|
|
||||||
// Creating successful results
|
|
||||||
var successResult = Result.Success(42);
|
|
||||||
var voidSuccessResult = Result.Success();
|
|
||||||
|
|
||||||
// Creating failed results
|
|
||||||
var failureResult = Result.Failure<int>("Something went wrong");
|
|
||||||
var exceptionResult = Result.Failure<int>(new ArgumentException("Invalid argument"));
|
|
||||||
|
|
||||||
// Creating failed results with error codes
|
|
||||||
var errorCodeResult = Result.Failure<int>(404L, "Not found");
|
|
||||||
var errorCodeWithExceptionResult = Result.Failure<int>(500L, "Internal server error", new Exception("Database connection failed"));
|
|
||||||
|
|
||||||
// Using Try patterns for safe execution
|
|
||||||
var tryResult = Result.Try(() => int.Parse("123"));
|
|
||||||
var tryFailResult = Result.Try(() => int.Parse("invalid"));
|
|
||||||
|
|
||||||
// Async Try patterns
|
|
||||||
var asyncResult = Result.TryAsync(async () => await SomeAsyncOperation());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods examples
|
|
||||||
/// </summary>
|
|
||||||
public static void ExtensionMethodsExamples()
|
|
||||||
{
|
|
||||||
var result = Result.Success(42);
|
|
||||||
|
|
||||||
// Safe value extraction
|
|
||||||
var nullableValue = result.GetOrNullable(); // Returns 42 for value types
|
|
||||||
var defaultValue = result.GetOrDefault(0); // Returns 42
|
|
||||||
var throwValue = result.GetOrThrow(); // Returns 42
|
|
||||||
|
|
||||||
// Alternative providers
|
|
||||||
var alternative = result.GetOrElse(() => 0);
|
|
||||||
var altResult = result.OrElse(() => Result.Success(0));
|
|
||||||
|
|
||||||
// Conversion methods
|
|
||||||
string? nullValue = null;
|
|
||||||
var nullResult = nullValue.AsResultOrFailure(); // Creates failure result
|
|
||||||
var valueResult = "test".AsResult(); // Creates success result
|
|
||||||
|
|
||||||
// Working with failures
|
|
||||||
var failResult = Result.Failure<int>("error");
|
|
||||||
var failNullable = failResult.GetOrNullable(); // Returns null
|
|
||||||
var failDefault = failResult.GetOrDefault(999); // Returns 999
|
|
||||||
var exception = failResult.ExceptionOrNull(); // Returns null (no exception, just message)
|
|
||||||
var errorMsg = failResult.ErrorMessageOrNull(); // Returns "error"
|
|
||||||
|
|
||||||
// Working with error codes
|
|
||||||
var errorCodeResult = Result.Failure<int>(404L, "Not found");
|
|
||||||
var errorCode = errorCodeResult.ErrorCodeOrNull(); // Returns 404
|
|
||||||
var hasSpecificCode = errorCodeResult.HasErrorCode(404L); // Returns true
|
|
||||||
var codeOrDefault = errorCodeResult.GetErrorCodeOrDefault(); // Returns 404
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Functional programming examples
|
|
||||||
/// </summary>
|
|
||||||
public static void FunctionalExamples()
|
|
||||||
{
|
|
||||||
var result = Result.Success(10);
|
|
||||||
|
|
||||||
// Map: Transform successful values
|
|
||||||
var doubled = result.Map(x => x * 2); // Result<int> with value 20
|
|
||||||
|
|
||||||
// Bind: Chain operations that return Results
|
|
||||||
var chained = result
|
|
||||||
.Bind(x => x > 5 ? Result.Success(x * 2) : Result.Failure<int>("Too small"))
|
|
||||||
.Bind(x => Result.Success(x.ToString()));
|
|
||||||
|
|
||||||
// Filter with Where/Ensure
|
|
||||||
var filtered = result
|
|
||||||
.Where(x => x > 5, "Value must be greater than 5")
|
|
||||||
.Ensure(x => x < 100, "Value must be less than 100");
|
|
||||||
|
|
||||||
// Filter with error codes
|
|
||||||
var filteredWithCode = result
|
|
||||||
.Where(x => x > 5, 400L, "Value must be greater than 5")
|
|
||||||
.Ensure(x => x < 100, 422L, "Value must be less than 100");
|
|
||||||
|
|
||||||
// Conditional execution
|
|
||||||
result.OnSuccess(value => Console.WriteLine($"Value: {value}"));
|
|
||||||
result.OnFailure((ex, msg) => Console.WriteLine($"Error: {msg}"));
|
|
||||||
result.OnFailure((ex, msg, code) => Console.WriteLine($"Error {code}: {msg}"));
|
|
||||||
|
|
||||||
// Pattern matching
|
|
||||||
var message = result.Match(
|
|
||||||
onSuccess: value => $"Success: {value}",
|
|
||||||
onFailure: (ex, msg) => $"Error: {msg}");
|
|
||||||
|
|
||||||
var messageWithCode = result.Match(
|
|
||||||
onSuccess: value => $"Success: {value}",
|
|
||||||
onFailure: (ex, msg, code) => $"Error {code}: {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// LINQ query syntax examples
|
|
||||||
/// </summary>
|
|
||||||
public static void LinqExamples()
|
|
||||||
{
|
|
||||||
// Using LINQ query syntax with Results
|
|
||||||
var result1 = Result.Success(5);
|
|
||||||
var result2 = Result.Success(10);
|
|
||||||
|
|
||||||
var combined = from x in result1
|
|
||||||
from y in result2
|
|
||||||
where x > 0 && y > 0
|
|
||||||
select x + y;
|
|
||||||
|
|
||||||
// More complex LINQ operations
|
|
||||||
var processed = from value in Result.Success("hello")
|
|
||||||
let upper = value.ToUpper()
|
|
||||||
where upper.Length > 3
|
|
||||||
select $"Processed: {upper}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Async operations examples
|
|
||||||
/// </summary>
|
|
||||||
public static async Task AsyncExamples()
|
|
||||||
{
|
|
||||||
// Async Try patterns
|
|
||||||
var asyncResult = await Result.TryAsync(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(100);
|
|
||||||
return 42;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Working with async results
|
|
||||||
if (asyncResult.IsSuccess)
|
|
||||||
{
|
|
||||||
await Task.Delay(50);
|
|
||||||
Console.WriteLine($"Processing: {asyncResult.Value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async mapping and binding
|
|
||||||
var mappedResult = await Task.FromResult(Result.Success(10))
|
|
||||||
.MapAsync(async value =>
|
|
||||||
{
|
|
||||||
await Task.Delay(50);
|
|
||||||
return value * 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
var boundResult = await Task.FromResult(Result.Success(5))
|
|
||||||
.BindAsync(async value =>
|
|
||||||
{
|
|
||||||
await Task.Delay(50);
|
|
||||||
return value > 0 ? Result.Success(value * 2) : Result.Failure<int>("Invalid");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Error handling patterns
|
|
||||||
/// </summary>
|
|
||||||
public static void ErrorHandlingExamples()
|
|
||||||
{
|
|
||||||
// Multiple validation steps
|
|
||||||
var input = "123";
|
|
||||||
var validationResult = input.AsResult()
|
|
||||||
.Ensure(s => !string.IsNullOrEmpty(s), "Input cannot be empty")
|
|
||||||
.Bind(s => Result.Try(() => int.Parse(s)))
|
|
||||||
.Ensure(i => i > 0, "Number must be positive")
|
|
||||||
.Map(i => i * 2);
|
|
||||||
|
|
||||||
// Handling different error scenarios
|
|
||||||
validationResult
|
|
||||||
.OnSuccess(value => Console.WriteLine($"Valid result: {value}"))
|
|
||||||
.OnFailure((ex, msg, code) =>
|
|
||||||
{
|
|
||||||
if (ex is FormatException)
|
|
||||||
Console.WriteLine("Invalid number format");
|
|
||||||
else if (code.HasValue)
|
|
||||||
Console.WriteLine($"Validation error {code}: {msg}");
|
|
||||||
else
|
|
||||||
Console.WriteLine($"Validation error: {msg}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Providing fallback values
|
|
||||||
var safeResult = validationResult.OrElse(() => Result.Success(0));
|
|
||||||
var finalValue = validationResult.GetOrElse(() => 0);
|
|
||||||
|
|
||||||
// Error code specific handling
|
|
||||||
var safeWithCodeHandling = validationResult.OrElse((ex, msg, code) =>
|
|
||||||
{
|
|
||||||
return code switch
|
|
||||||
{
|
|
||||||
400L => Result.Success(0), // Bad request -> default
|
|
||||||
422L => Result.Success(-1), // Validation -> sentinel
|
|
||||||
_ => Result.Success(999) // Unknown -> special value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Integration with existing code examples
|
|
||||||
/// </summary>
|
|
||||||
public static void IntegrationExamples()
|
|
||||||
{
|
|
||||||
// Converting existing exception-throwing code
|
|
||||||
var legacyResult = Result.Try(() => RiskyOperation());
|
|
||||||
|
|
||||||
// Working with nullable types
|
|
||||||
string? nullableString = GetNullableString();
|
|
||||||
var nullableResult = nullableString.AsResultOrFailure("String was null");
|
|
||||||
|
|
||||||
// Converting Result back to exceptions when needed
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = legacyResult.GetOrThrow();
|
|
||||||
// Use value...
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Handle exception...
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignoring typed results when only success/failure matters
|
|
||||||
var voidResult = Result.Success(42).Ignore();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Error code specific examples
|
|
||||||
/// </summary>
|
|
||||||
public static void ErrorCodeExamples()
|
|
||||||
{
|
|
||||||
// Creating failures with error codes
|
|
||||||
var notFoundResult = Result.Failure<string>(404L, "Resource not found");
|
|
||||||
var validationResult = Result.Failure<int>(422L, "Invalid input");
|
|
||||||
var serverError = Result.Failure<object>(500L, "Internal server error", new Exception("Database connection failed"));
|
|
||||||
|
|
||||||
// Using ExceptionExpand with error codes
|
|
||||||
var customException = new ExceptionExpand(1001L, "Custom business error", "Check your input parameters");
|
|
||||||
var fromException = ResultExtensions.FromExceptionExpand<string>(customException);
|
|
||||||
|
|
||||||
// Error code checking and handling
|
|
||||||
if (notFoundResult.HasErrorCode(404L))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Handle not found error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch on error codes
|
|
||||||
var errorCode = validationResult.GetErrorCodeOrDefault();
|
|
||||||
var response = errorCode switch
|
|
||||||
{
|
|
||||||
404L => "Resource not found",
|
|
||||||
422L => "Validation failed",
|
|
||||||
500L => "Server error",
|
|
||||||
_ => "Unknown error"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Error code in functional chains
|
|
||||||
var processedResult = Result.Success("user@example.com")
|
|
||||||
.Ensure(email => email.Contains("@"), 400L, "Invalid email format")
|
|
||||||
.Bind(email => ValidateEmailDomain(email))
|
|
||||||
.Map(email => $"Welcome, {email}");
|
|
||||||
|
|
||||||
// Handling specific error codes
|
|
||||||
processedResult.OnFailure((ex, msg, code) =>
|
|
||||||
{
|
|
||||||
var logLevel = code switch
|
|
||||||
{
|
|
||||||
400L => "WARN", // Validation errors
|
|
||||||
404L => "INFO", // Not found
|
|
||||||
500L => "ERROR", // Server errors
|
|
||||||
_ => "DEBUG"
|
|
||||||
};
|
|
||||||
Console.WriteLine($"[{logLevel}] Error {code}: {msg}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Creating error code specific results
|
|
||||||
var unauthorizedResult = ResultExtensions.FailWithCode<string>(401L, "Unauthorized access");
|
|
||||||
var forbiddenResult = Result.Failure<string>(403L, "Insufficient permissions");
|
|
||||||
|
|
||||||
// Combining results with error code preservation
|
|
||||||
var combinedResult = from user in GetUserById(123)
|
|
||||||
from permissions in GetUserPermissions(user)
|
|
||||||
where permissions.CanRead
|
|
||||||
select user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP status code style error handling
|
|
||||||
/// </summary>
|
|
||||||
public static void HttpStatusCodeStyleExamples()
|
|
||||||
{
|
|
||||||
// Common HTTP status codes as error codes
|
|
||||||
var badRequest = Result.Failure<string>(400L, "Bad Request");
|
|
||||||
var unauthorized = Result.Failure<string>(401L, "Unauthorized");
|
|
||||||
var forbidden = Result.Failure<string>(403L, "Forbidden");
|
|
||||||
var notFound = Result.Failure<string>(404L, "Not Found");
|
|
||||||
var conflict = Result.Failure<string>(409L, "Conflict");
|
|
||||||
var unprocessable = Result.Failure<string>(422L, "Unprocessable Entity");
|
|
||||||
var internalError = Result.Failure<string>(500L, "Internal Server Error");
|
|
||||||
|
|
||||||
// Business logic error codes
|
|
||||||
var businessError = Result.Failure<string>(1001L, "Insufficient balance");
|
|
||||||
var domainError = Result.Failure<string>(2001L, "Invalid user state");
|
|
||||||
var integrationError = Result.Failure<string>(3001L, "External service unavailable");
|
|
||||||
|
|
||||||
// Error code ranges for different error types
|
|
||||||
var validationError = Result.Failure<string>(4001L, "Required field missing"); // 4xxx: Validation
|
|
||||||
var businessLogicError = Result.Failure<string>(5001L, "Business rule violation"); // 5xxx: Business
|
|
||||||
var systemError = Result.Failure<string>(6001L, "Database connection failed"); // 6xxx: System
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for examples
|
|
||||||
private static int RiskyOperation() => throw new InvalidOperationException("Something went wrong");
|
|
||||||
private static string? GetNullableString() => null;
|
|
||||||
private static async Task<string> SomeAsyncOperation()
|
|
||||||
{
|
|
||||||
await Task.Delay(100);
|
|
||||||
return "async result";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Result<string> ValidateEmailDomain(string email)
|
|
||||||
{
|
|
||||||
var domain = email.Split('@').LastOrDefault();
|
|
||||||
return domain switch
|
|
||||||
{
|
|
||||||
"blocked.com" => Result.Failure<string>(403L, "Domain is blocked"),
|
|
||||||
null => Result.Failure<string>(400L, "Invalid email format"),
|
|
||||||
_ => Result.Success(email)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Result<User> GetUserById(int id)
|
|
||||||
{
|
|
||||||
return id switch
|
|
||||||
{
|
|
||||||
123 => Result.Success(new User { Id = 123, Name = "John" }),
|
|
||||||
_ => Result.Failure<User>(404L, "User not found")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Result<UserPermissions> GetUserPermissions(User user)
|
|
||||||
{
|
|
||||||
return Result.Success(new UserPermissions { CanRead = true, CanWrite = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private class User
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UserPermissions
|
|
||||||
{
|
|
||||||
public bool CanRead { get; set; }
|
|
||||||
public bool CanWrite { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Results;
|
||||||
|
|
||||||
|
namespace Common.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证工具类
|
||||||
|
/// 提供常见数据验证功能,并与 Result 模式集成
|
||||||
|
/// </summary>
|
||||||
|
public static class ValidationHelper
|
||||||
|
{
|
||||||
|
#region 邮箱验证
|
||||||
|
|
||||||
|
private static readonly Regex EmailRegex = new(
|
||||||
|
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证邮箱地址格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">邮箱地址</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsValidEmail(string? email)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return EmailRegex.IsMatch(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证邮箱地址格式(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">邮箱地址</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateEmail(string? email, long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
return Result.Failure<string>(errorCode, "邮箱地址不能为空");
|
||||||
|
|
||||||
|
if (!EmailRegex.IsMatch(email))
|
||||||
|
return Result.Failure<string>(errorCode, "邮箱地址格式无效");
|
||||||
|
|
||||||
|
return Result.Success(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 手机号验证
|
||||||
|
|
||||||
|
// 中国大陆手机号:1 开头,第二位是 3-9,共 11 位
|
||||||
|
private static readonly Regex ChinaMobileRegex = new(
|
||||||
|
@"^1[3-9]\d{9}$",
|
||||||
|
RegexOptions.Compiled
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证中国大陆手机号格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mobile">手机号</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsValidChinaMobile(string? mobile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ChinaMobileRegex.IsMatch(mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证中国大陆手机号格式(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mobile">手机号</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateChinaMobile(string? mobile, long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile))
|
||||||
|
return Result.Failure<string>(errorCode, "手机号不能为空");
|
||||||
|
|
||||||
|
if (!ChinaMobileRegex.IsMatch(mobile))
|
||||||
|
return Result.Failure<string>(errorCode, "手机号格式无效(应为11位数字,1开头)");
|
||||||
|
|
||||||
|
return Result.Success(mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证国际手机号格式(包含国际区号)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mobile">手机号(格式:+区号-号码,如 +86-13812345678)</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsValidInternationalMobile(string? mobile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 匹配 +区号-号码 或 +区号号码 格式
|
||||||
|
var regex = new Regex(@"^\+\d{1,3}[-\s]?\d{6,14}$");
|
||||||
|
return regex.IsMatch(mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 身份证验证
|
||||||
|
|
||||||
|
// 18 位身份证号(最后一位可以是数字或 X)
|
||||||
|
private static readonly Regex IdCardRegex = new(
|
||||||
|
@"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$",
|
||||||
|
RegexOptions.Compiled
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证中国大陆身份证号格式(18位)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsValidChinaIdCard(string? idCard)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idCard))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!IdCardRegex.IsMatch(idCard))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 验证校验码
|
||||||
|
return ValidateIdCardChecksum(idCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证中国大陆身份证号格式(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateChinaIdCard(string? idCard, long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idCard))
|
||||||
|
return Result.Failure<string>(errorCode, "身份证号不能为空");
|
||||||
|
|
||||||
|
if (!IdCardRegex.IsMatch(idCard))
|
||||||
|
return Result.Failure<string>(errorCode, "身份证号格式无效(应为18位)");
|
||||||
|
|
||||||
|
if (!ValidateIdCardChecksum(idCard))
|
||||||
|
return Result.Failure<string>(errorCode, "身份证号校验码无效");
|
||||||
|
|
||||||
|
return Result.Success(idCard.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ValidateIdCardChecksum(string idCard)
|
||||||
|
{
|
||||||
|
if (idCard.Length != 18)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 加权因子
|
||||||
|
int[] weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
|
||||||
|
// 校验码对应值
|
||||||
|
char[] checkCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
|
||||||
|
|
||||||
|
int sum = 0;
|
||||||
|
for (int i = 0; i < 17; i++)
|
||||||
|
{
|
||||||
|
if (!char.IsDigit(idCard[i]))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
sum += (idCard[i] - '0') * weights[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
int mod = sum % 11;
|
||||||
|
char expectedCheckCode = checkCodes[mod];
|
||||||
|
char actualCheckCode = char.ToUpperInvariant(idCard[17]);
|
||||||
|
|
||||||
|
return expectedCheckCode == actualCheckCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从身份证号提取出生日期
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <returns>出生日期(如果格式无效返回 null)</returns>
|
||||||
|
public static DateTime? ExtractBirthDateFromIdCard(string? idCard)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idCard) || idCard.Length != 18)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int year = int.Parse(idCard.Substring(6, 4));
|
||||||
|
int month = int.Parse(idCard.Substring(10, 2));
|
||||||
|
int day = int.Parse(idCard.Substring(12, 2));
|
||||||
|
|
||||||
|
return new DateTime(year, month, day);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从身份证号提取性别
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <returns>性别(Male/Female,如果格式无效返回 null)</returns>
|
||||||
|
public static string? ExtractGenderFromIdCard(string? idCard)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idCard) || idCard.Length != 18)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int genderCode = int.Parse(idCard.Substring(16, 1));
|
||||||
|
return genderCode % 2 == 0 ? "Female" : "Male";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region URL 验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 URL 格式(使用 Uri.TryCreate)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsValidUrl(string? url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
|
||||||
|
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 URL 格式(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateUrl(string? url, long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return Result.Failure<string>(errorCode, "URL 不能为空");
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult))
|
||||||
|
return Result.Failure<string>(errorCode, "URL 格式无效");
|
||||||
|
|
||||||
|
if (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps)
|
||||||
|
return Result.Failure<string>(errorCode, "URL 必须是 HTTP 或 HTTPS 协议");
|
||||||
|
|
||||||
|
return Result.Success(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 数值范围验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证整数在指定范围内(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">要验证的值</param>
|
||||||
|
/// <param name="min">最小值(包含)</param>
|
||||||
|
/// <param name="max">最大值(包含)</param>
|
||||||
|
/// <param name="fieldName">字段名称</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<int> ValidateRange(int value, int min, int max, string fieldName = "值", long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (value < min || value > max)
|
||||||
|
return Result.Failure<int>(errorCode, $"{fieldName}必须在 {min} 到 {max} 之间");
|
||||||
|
|
||||||
|
return Result.Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证浮点数在指定范围内(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
public static Result<double> ValidateRange(double value, double min, double max, string fieldName = "值", long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (value < min || value > max)
|
||||||
|
return Result.Failure<double>(errorCode, $"{fieldName}必须在 {min} 到 {max} 之间");
|
||||||
|
|
||||||
|
return Result.Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串长度验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证字符串长度(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">字符串值</param>
|
||||||
|
/// <param name="minLength">最小长度</param>
|
||||||
|
/// <param name="maxLength">最大长度</param>
|
||||||
|
/// <param name="fieldName">字段名称</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateLength(string? value, int minLength, int maxLength, string fieldName = "字段", long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return Result.Failure<string>(errorCode, $"{fieldName}不能为空");
|
||||||
|
|
||||||
|
if (value.Length < minLength)
|
||||||
|
return Result.Failure<string>(errorCode, $"{fieldName}长度不能少于 {minLength} 个字符");
|
||||||
|
|
||||||
|
if (value.Length > maxLength)
|
||||||
|
return Result.Failure<string>(errorCode, $"{fieldName}长度不能超过 {maxLength} 个字符");
|
||||||
|
|
||||||
|
return Result.Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证字符串非空(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
public static Result<string> ValidateNotEmpty(string? value, string fieldName = "字段", long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return Result.Failure<string>(errorCode, $"{fieldName}不能为空");
|
||||||
|
|
||||||
|
return Result.Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 密码强度验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证密码强度
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <param name="minLength">最小长度(默认 8)</param>
|
||||||
|
/// <param name="requireDigit">是否必须包含数字</param>
|
||||||
|
/// <param name="requireLowercase">是否必须包含小写字母</param>
|
||||||
|
/// <param name="requireUppercase">是否必须包含大写字母</param>
|
||||||
|
/// <param name="requireSpecialChar">是否必须包含特殊字符</param>
|
||||||
|
/// <returns>是否符合强度要求</returns>
|
||||||
|
public static bool IsStrongPassword(
|
||||||
|
string? password,
|
||||||
|
int minLength = 8,
|
||||||
|
bool requireDigit = true,
|
||||||
|
bool requireLowercase = true,
|
||||||
|
bool requireUppercase = true,
|
||||||
|
bool requireSpecialChar = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (password.Length < minLength)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requireDigit && !password.Any(char.IsDigit))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requireLowercase && !password.Any(char.IsLower))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requireUppercase && !password.Any(char.IsUpper))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requireSpecialChar && !password.Any(c => !char.IsLetterOrDigit(c)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证密码强度(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
public static Result<string> ValidatePassword(
|
||||||
|
string? password,
|
||||||
|
int minLength = 8,
|
||||||
|
bool requireDigit = true,
|
||||||
|
bool requireLowercase = true,
|
||||||
|
bool requireUppercase = true,
|
||||||
|
bool requireSpecialChar = true,
|
||||||
|
long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
return Result.Failure<string>(errorCode, "密码不能为空");
|
||||||
|
|
||||||
|
if (password.Length < minLength)
|
||||||
|
return Result.Failure<string>(errorCode, $"密码长度不能少于 {minLength} 个字符");
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (requireDigit && !password.Any(char.IsDigit))
|
||||||
|
errors.Add("至少包含一个数字");
|
||||||
|
|
||||||
|
if (requireLowercase && !password.Any(char.IsLower))
|
||||||
|
errors.Add("至少包含一个小写字母");
|
||||||
|
|
||||||
|
if (requireUppercase && !password.Any(char.IsUpper))
|
||||||
|
errors.Add("至少包含一个大写字母");
|
||||||
|
|
||||||
|
if (requireSpecialChar && !password.Any(c => !char.IsLetterOrDigit(c)))
|
||||||
|
errors.Add("至少包含一个特殊字符");
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
return Result.Failure<string>(errorCode, $"密码强度不足:{string.Join("、", errors)}");
|
||||||
|
|
||||||
|
return Result.Success(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 通用正则表达式验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用自定义正则表达式验证
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">要验证的值</param>
|
||||||
|
/// <param name="pattern">正则表达式模式</param>
|
||||||
|
/// <param name="errorMessage">错误消息</param>
|
||||||
|
/// <param name="errorCode">错误码(默认 400)</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<string> ValidateRegex(string? value, string pattern, string errorMessage = "格式无效", long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return Result.Failure<string>(errorCode, "值不能为空");
|
||||||
|
|
||||||
|
var regex = new Regex(pattern);
|
||||||
|
if (!regex.IsMatch(value))
|
||||||
|
return Result.Failure<string>(errorCode, errorMessage);
|
||||||
|
|
||||||
|
return Result.Success(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IP 地址验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 IPv4 地址格式
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsValidIPv4(string? ip)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ip))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return System.Net.IPAddress.TryParse(ip, out var address)
|
||||||
|
&& address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 IP 地址格式(Result 模式)
|
||||||
|
/// </summary>
|
||||||
|
public static Result<string> ValidateIPAddress(string? ip, long errorCode = 400)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ip))
|
||||||
|
return Result.Failure<string>(errorCode, "IP 地址不能为空");
|
||||||
|
|
||||||
|
if (!System.Net.IPAddress.TryParse(ip, out _))
|
||||||
|
return Result.Failure<string>(errorCode, "IP 地址格式无效");
|
||||||
|
|
||||||
|
return Result.Success(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 组合验证
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组合多个验证规则(所有规则都必须通过)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">值类型</typeparam>
|
||||||
|
/// <param name="value">要验证的值</param>
|
||||||
|
/// <param name="validators">验证器列表</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<T> ValidateAll<T>(T value, params Func<T, Result<T>>[] validators)
|
||||||
|
{
|
||||||
|
var result = Result.Success(value);
|
||||||
|
|
||||||
|
foreach (var validator in validators)
|
||||||
|
{
|
||||||
|
result = result.Bind(validator);
|
||||||
|
if (result.IsFailure)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组合多个验证规则(任一规则通过即可)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">值类型</typeparam>
|
||||||
|
/// <param name="value">要验证的值</param>
|
||||||
|
/// <param name="validators">验证器列表</param>
|
||||||
|
/// <returns>验证结果</returns>
|
||||||
|
public static Result<T> ValidateAny<T>(T value, params Func<T, Result<T>>[] validators)
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
foreach (var validator in validators)
|
||||||
|
{
|
||||||
|
var result = validator(value);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
if (result.ErrorMessage != null)
|
||||||
|
errors.Add(result.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Failure<T>($"所有验证规则都未通过:{string.Join("; ", errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,748 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CryptoHelper 加密工具测试
|
||||||
|
/// 展示各种加密、哈希、编码功能
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class CryptoHelperTests
|
||||||
|
{
|
||||||
|
#region AES 加密解密测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:AES 加密和解密
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AesEncrypt_AndDecrypt_ReturnsOriginalText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "Hello, World! 你好世界!";
|
||||||
|
var password = "MySecretPassword123";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encrypted = CryptoHelper.AesEncrypt(plainText, password);
|
||||||
|
var decrypted = CryptoHelper.AesDecrypt(encrypted, password);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreNotEqual(plainText, encrypted);
|
||||||
|
Assert.AreEqual(plainText, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:相同内容使用相同密码多次加密,结果应该不同(因为 Salt 随机)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AesEncrypt_SameTextSamePassword_ProducesDifferentCiphertext()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "Test Message";
|
||||||
|
var password = "password";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encrypted1 = CryptoHelper.AesEncrypt(plainText, password);
|
||||||
|
var encrypted2 = CryptoHelper.AesEncrypt(plainText, password);
|
||||||
|
|
||||||
|
// Assert: 密文应该不同(因为使用了随机 Salt)
|
||||||
|
Assert.AreNotEqual(encrypted1, encrypted2);
|
||||||
|
|
||||||
|
// 但都应该能正确解密
|
||||||
|
Assert.AreEqual(plainText, CryptoHelper.AesDecrypt(encrypted1, password));
|
||||||
|
Assert.AreEqual(plainText, CryptoHelper.AesDecrypt(encrypted2, password));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:使用错误密码解密失败
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AesDecrypt_WrongPassword_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "Secret Message";
|
||||||
|
var correctPassword = "correct";
|
||||||
|
var wrongPassword = "wrong";
|
||||||
|
|
||||||
|
var encrypted = CryptoHelper.AesEncrypt(plainText, correctPassword);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsException<CryptographicException>(() =>
|
||||||
|
{
|
||||||
|
CryptoHelper.AesDecrypt(encrypted, wrongPassword);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:加密空字符串抛出异常
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AesEncrypt_EmptyString_ThrowsException()
|
||||||
|
{
|
||||||
|
Assert.ThrowsException<ArgumentNullException>(() =>
|
||||||
|
{
|
||||||
|
CryptoHelper.AesEncrypt("", "password");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region RSA 加密解密测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:生成 RSA 密钥对
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GenerateRsaKeys_ReturnsPublicAndPrivateKeys()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var (publicKey, privateKey) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(string.IsNullOrEmpty(publicKey));
|
||||||
|
Assert.IsFalse(string.IsNullOrEmpty(privateKey));
|
||||||
|
Assert.AreNotEqual(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:RSA 公钥加密,私钥解密
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RsaEncrypt_AndDecrypt_ReturnsOriginalText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "RSA Test Message";
|
||||||
|
var (publicKey, privateKey) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encrypted = CryptoHelper.RsaEncrypt(plainText, publicKey);
|
||||||
|
var decrypted = CryptoHelper.RsaDecrypt(encrypted, privateKey);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreNotEqual(plainText, encrypted);
|
||||||
|
Assert.AreEqual(plainText, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:RSA 加密相同内容产生不同密文(OAEP 填充)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RsaEncrypt_SameText_ProducesDifferentCiphertext()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "Test";
|
||||||
|
var (publicKey, _) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encrypted1 = CryptoHelper.RsaEncrypt(plainText, publicKey);
|
||||||
|
var encrypted2 = CryptoHelper.RsaEncrypt(plainText, publicKey);
|
||||||
|
|
||||||
|
// Assert: 由于 OAEP 填充使用随机数,密文应该不同
|
||||||
|
Assert.AreNotEqual(encrypted1, encrypted2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:使用错误的私钥解密失败
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RsaDecrypt_WrongPrivateKey_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var plainText = "Secret";
|
||||||
|
var (publicKey1, _) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
var (_, privateKey2) = CryptoHelper.GenerateRsaKeys(); // 不同的密钥对
|
||||||
|
|
||||||
|
var encrypted = CryptoHelper.RsaEncrypt(plainText, publicKey1);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsException<CryptographicException>(() =>
|
||||||
|
{
|
||||||
|
CryptoHelper.RsaDecrypt(encrypted, privateKey2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 哈希函数测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:MD5 哈希
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Md5Hash_ProducesCorrectHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Hello, World!";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash = CryptoHelper.Md5Hash(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(32, hash.Length); // MD5 产生 32 位十六进制字符串
|
||||||
|
Assert.AreEqual("65a8e27d8879283831b664bd8b7f0ad4", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:SHA256 哈希
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Sha256Hash_ProducesCorrectHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Hello, World!";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash = CryptoHelper.Sha256Hash(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(64, hash.Length); // SHA256 产生 64 位十六进制字符串
|
||||||
|
Assert.AreEqual("dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:SHA512 哈希
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Sha512Hash_ProducesCorrectHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Hello, World!";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash = CryptoHelper.Sha512Hash(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(128, hash.Length); // SHA512 产生 128 位十六进制字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:相同输入产生相同哈希
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Hash_SameInput_ProducesSameHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Test Input";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var md5_1 = CryptoHelper.Md5Hash(input);
|
||||||
|
var md5_2 = CryptoHelper.Md5Hash(input);
|
||||||
|
var sha256_1 = CryptoHelper.Sha256Hash(input);
|
||||||
|
var sha256_2 = CryptoHelper.Sha256Hash(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(md5_1, md5_2);
|
||||||
|
Assert.AreEqual(sha256_1, sha256_2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:不同输入产生不同哈希
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Hash_DifferentInput_ProducesDifferentHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input1 = "Input1";
|
||||||
|
var input2 = "Input2";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash1 = CryptoHelper.Sha256Hash(input1);
|
||||||
|
var hash2 = CryptoHelper.Sha256Hash(input2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreNotEqual(hash1, hash2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Base64 编解码测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:Base64 编码和解码
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Base64Encode_AndDecode_ReturnsOriginalText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Hello, World! 你好世界!";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encoded = CryptoHelper.Base64Encode(input);
|
||||||
|
var decoded = CryptoHelper.Base64Decode(encoded);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreNotEqual(input, encoded);
|
||||||
|
Assert.AreEqual(input, decoded);
|
||||||
|
Assert.AreEqual("SGVsbG8sIFdvcmxkISDkvaDlpb3kuJbnlYzvvIE=", encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:URL 安全的 Base64 编码
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Base64UrlEncode_ProducesUrlSafeString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "Hello+World/Test=";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encoded = CryptoHelper.Base64UrlEncode(input);
|
||||||
|
var decoded = CryptoHelper.Base64UrlDecode(encoded);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(encoded.Contains('+')); // 不包含 +
|
||||||
|
Assert.IsFalse(encoded.Contains('/')); // 不包含 /
|
||||||
|
Assert.IsFalse(encoded.Contains('=')); // 不包含 =
|
||||||
|
Assert.AreEqual(input, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:URL Base64 编解码
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Base64UrlEncode_AndDecode_ReturnsOriginalText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testCases = new[]
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"test1",
|
||||||
|
"test12",
|
||||||
|
"test123",
|
||||||
|
"Hello, World!",
|
||||||
|
"特殊字符 !@#$%^&*()"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var input in testCases)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var encoded = CryptoHelper.Base64UrlEncode(input);
|
||||||
|
var decoded = CryptoHelper.Base64UrlDecode(encoded);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(input, decoded, $"Failed for input: {input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 密码哈希测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:哈希密码并验证
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HashPassword_AndVerify_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var password = "MySecurePassword123!";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hashed = CryptoHelper.HashPassword(password);
|
||||||
|
var isValid = CryptoHelper.VerifyPassword(password, hashed);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(isValid);
|
||||||
|
Assert.AreNotEqual(password, hashed);
|
||||||
|
Assert.IsTrue(hashed.StartsWith("$pbkdf2-sha256$"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:验证错误密码返回 false
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void VerifyPassword_WrongPassword_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var correctPassword = "correct";
|
||||||
|
var wrongPassword = "wrong";
|
||||||
|
var hashed = CryptoHelper.HashPassword(correctPassword);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isValid = CryptoHelper.VerifyPassword(wrongPassword, hashed);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:相同密码多次哈希产生不同结果
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HashPassword_SamePassword_ProducesDifferentHashes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var password = "password123";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash1 = CryptoHelper.HashPassword(password);
|
||||||
|
var hash2 = CryptoHelper.HashPassword(password);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreNotEqual(hash1, hash2); // 不同的 Salt
|
||||||
|
|
||||||
|
// 但都应该能验证通过
|
||||||
|
Assert.IsTrue(CryptoHelper.VerifyPassword(password, hash1));
|
||||||
|
Assert.IsTrue(CryptoHelper.VerifyPassword(password, hash2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:自定义哈希参数
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HashPassword_CustomParameters_Works()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var password = "test";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var hash = CryptoHelper.HashPassword(password, saltSize: 32, iterations: 50000, hashSize: 64);
|
||||||
|
var isValid = CryptoHelper.VerifyPassword(password, hash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region HMAC 签名测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:HMAC-SHA256 签名和验证
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HmacSha256_AndVerify_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "Important message";
|
||||||
|
var secret = "secret-key";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var signature = CryptoHelper.HmacSha256(message, secret);
|
||||||
|
var isValid = CryptoHelper.VerifyHmacSha256(message, signature, secret);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(isValid);
|
||||||
|
Assert.AreEqual(64, signature.Length); // SHA256 产生 64 位十六进制
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:验证错误签名返回 false
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void VerifyHmacSha256_WrongSignature_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "message";
|
||||||
|
var secret = "secret";
|
||||||
|
var wrongSignature = "wrong_signature";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isValid = CryptoHelper.VerifyHmacSha256(message, wrongSignature, secret);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:修改消息后签名无效
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void VerifyHmacSha256_ModifiedMessage_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalMessage = "Original message";
|
||||||
|
var modifiedMessage = "Modified message";
|
||||||
|
var secret = "secret";
|
||||||
|
|
||||||
|
var signature = CryptoHelper.HmacSha256(originalMessage, secret);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isValid = CryptoHelper.VerifyHmacSha256(modifiedMessage, signature, secret);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:HMAC-SHA512 签名
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HmacSha512_ProducesCorrectSignature()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "Test message";
|
||||||
|
var secret = "secret";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var signature = CryptoHelper.HmacSha512(message, secret);
|
||||||
|
var isValid = CryptoHelper.VerifyHmacSha512(message, signature, secret);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(isValid);
|
||||||
|
Assert.AreEqual(128, signature.Length); // SHA512 产生 128 位十六进制
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:相同消息和密钥产生相同签名
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void HmacSha256_SameInputs_ProducesSameSignature()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "message";
|
||||||
|
var secret = "secret";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var signature1 = CryptoHelper.HmacSha256(message, secret);
|
||||||
|
var signature2 = CryptoHelper.HmacSha256(message, secret);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(signature1, signature2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助方法测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:生成随机字符串
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GenerateRandomString_ProducesCorrectLength()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var str10 = CryptoHelper.GenerateRandomString(10);
|
||||||
|
var str20 = CryptoHelper.GenerateRandomString(20);
|
||||||
|
var strWithSpecial = CryptoHelper.GenerateRandomString(15, useSpecialChars: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(10, str10.Length);
|
||||||
|
Assert.AreEqual(20, str20.Length);
|
||||||
|
Assert.AreEqual(15, strWithSpecial.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:生成随机密码
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratePassword_MeetsRequirements()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var password = CryptoHelper.GeneratePassword(16);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(16, password.Length);
|
||||||
|
Assert.IsTrue(password.Any(char.IsDigit), "Should contain digit");
|
||||||
|
Assert.IsTrue(password.Any(char.IsLower), "Should contain lowercase");
|
||||||
|
Assert.IsTrue(password.Any(char.IsUpper), "Should contain uppercase");
|
||||||
|
Assert.IsTrue(password.Any(c => !char.IsLetterOrDigit(c)), "Should contain special char");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:生成仅包含字母和数字的密码
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratePassword_OnlyAlphanumeric_NoSpecialChars()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var password = CryptoHelper.GeneratePassword(
|
||||||
|
12,
|
||||||
|
requireDigit: true,
|
||||||
|
requireLowercase: true,
|
||||||
|
requireUppercase: true,
|
||||||
|
requireSpecialChar: false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(12, password.Length);
|
||||||
|
Assert.IsTrue(password.All(c => char.IsLetterOrDigit(c)), "Should only contain alphanumeric");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:生成多个随机字符串应该不同
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GenerateRandomString_MultipleCalls_ProducesDifferentResults()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var strings = Enumerable.Range(0, 10)
|
||||||
|
.Select(_ => CryptoHelper.GenerateRandomString(20))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Assert: 所有字符串应该不同
|
||||||
|
Assert.AreEqual(10, strings.Distinct().Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:用户密码存储和验证
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_UserPasswordStorageAndVerification()
|
||||||
|
{
|
||||||
|
// 模拟用户注册
|
||||||
|
var userPassword = "SecurePassword123!";
|
||||||
|
var hashedPassword = CryptoHelper.HashPassword(userPassword);
|
||||||
|
|
||||||
|
// 存储 hashedPassword 到数据库...
|
||||||
|
|
||||||
|
// 模拟用户登录
|
||||||
|
var loginPassword = "SecurePassword123!";
|
||||||
|
var isAuthenticated = CryptoHelper.VerifyPassword(loginPassword, hashedPassword);
|
||||||
|
|
||||||
|
Assert.IsTrue(isAuthenticated);
|
||||||
|
|
||||||
|
// 错误密码
|
||||||
|
var wrongPassword = "WrongPassword";
|
||||||
|
var isFailed = CryptoHelper.VerifyPassword(wrongPassword, hashedPassword);
|
||||||
|
|
||||||
|
Assert.IsFalse(isFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:API 签名验证
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_ApiSignatureVerification()
|
||||||
|
{
|
||||||
|
// 客户端准备 API 请求
|
||||||
|
var apiKey = "client-api-key";
|
||||||
|
var apiSecret = "client-api-secret";
|
||||||
|
var requestBody = "{\"userId\":123,\"action\":\"purchase\"}";
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
var signatureData = $"{apiKey}{requestBody}{timestamp}";
|
||||||
|
var signature = CryptoHelper.HmacSha256(signatureData, apiSecret);
|
||||||
|
|
||||||
|
// 发送请求(包含 signature 和 timestamp)...
|
||||||
|
|
||||||
|
// 服务器端验证签名
|
||||||
|
var receivedSignatureData = $"{apiKey}{requestBody}{timestamp}";
|
||||||
|
var isValidSignature = CryptoHelper.VerifyHmacSha256(
|
||||||
|
receivedSignatureData,
|
||||||
|
signature,
|
||||||
|
apiSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.IsTrue(isValidSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:敏感数据加密存储
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_EncryptSensitiveData()
|
||||||
|
{
|
||||||
|
// 加密敏感数据(如信用卡号)
|
||||||
|
var creditCardNumber = "4532-1234-5678-9010";
|
||||||
|
var encryptionKey = "DataEncryptionKey123!";
|
||||||
|
|
||||||
|
var encrypted = CryptoHelper.AesEncrypt(creditCardNumber, encryptionKey);
|
||||||
|
|
||||||
|
// 存储加密后的数据...
|
||||||
|
Console.WriteLine($"Encrypted: {encrypted}");
|
||||||
|
|
||||||
|
// 需要使用时解密
|
||||||
|
var decrypted = CryptoHelper.AesDecrypt(encrypted, encryptionKey);
|
||||||
|
|
||||||
|
Assert.AreEqual(creditCardNumber, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:文件完整性验证
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_FileIntegrityVerification()
|
||||||
|
{
|
||||||
|
// 模拟文件内容
|
||||||
|
var fileContent = "Important file content that should not be modified";
|
||||||
|
|
||||||
|
// 计算文件哈希
|
||||||
|
var originalHash = CryptoHelper.Sha256Hash(fileContent);
|
||||||
|
|
||||||
|
// 存储哈希值...
|
||||||
|
|
||||||
|
// 后续验证文件是否被篡改
|
||||||
|
var currentHash = CryptoHelper.Sha256Hash(fileContent);
|
||||||
|
|
||||||
|
Assert.AreEqual(originalHash, currentHash, "File has not been modified");
|
||||||
|
|
||||||
|
// 模拟文件被修改
|
||||||
|
var modifiedContent = fileContent + " (modified)";
|
||||||
|
var modifiedHash = CryptoHelper.Sha256Hash(modifiedContent);
|
||||||
|
|
||||||
|
Assert.AreNotEqual(originalHash, modifiedHash, "File has been modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:JWT Token 签名(简化版)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_JwtTokenSignature()
|
||||||
|
{
|
||||||
|
// JWT Header 和 Payload
|
||||||
|
var header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
||||||
|
var payload = "{\"userId\":123,\"exp\":1234567890}";
|
||||||
|
|
||||||
|
var headerEncoded = CryptoHelper.Base64UrlEncode(header);
|
||||||
|
var payloadEncoded = CryptoHelper.Base64UrlEncode(payload);
|
||||||
|
|
||||||
|
var tokenWithoutSignature = $"{headerEncoded}.{payloadEncoded}";
|
||||||
|
|
||||||
|
// 使用 HMAC 签名
|
||||||
|
var secret = "jwt-secret-key";
|
||||||
|
var signature = CryptoHelper.HmacSha256(tokenWithoutSignature, secret);
|
||||||
|
var signatureEncoded = CryptoHelper.Base64UrlEncode(signature);
|
||||||
|
|
||||||
|
var fullToken = $"{tokenWithoutSignature}.{signatureEncoded}";
|
||||||
|
|
||||||
|
Console.WriteLine($"JWT Token: {fullToken}");
|
||||||
|
|
||||||
|
// 验证 Token
|
||||||
|
var parts = fullToken.Split('.');
|
||||||
|
var receivedSignature = CryptoHelper.Base64UrlDecode(parts[2]);
|
||||||
|
var expectedSignature = CryptoHelper.HmacSha256($"{parts[0]}.{parts[1]}", secret);
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedSignature, receivedSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:安全通信(RSA + AES 混合加密)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_HybridEncryption()
|
||||||
|
{
|
||||||
|
// 场景:发送大量数据,使用混合加密(RSA 加密 AES 密钥,AES 加密数据)
|
||||||
|
|
||||||
|
// 1. 接收方生成 RSA 密钥对
|
||||||
|
var (publicKey, privateKey) = CryptoHelper.GenerateRsaKeys();
|
||||||
|
|
||||||
|
// 2. 发送方生成随机 AES 密钥
|
||||||
|
var aesKey = CryptoHelper.GenerateRandomString(32);
|
||||||
|
|
||||||
|
// 3. 使用 RSA 公钥加密 AES 密钥
|
||||||
|
var encryptedAesKey = CryptoHelper.RsaEncrypt(aesKey, publicKey);
|
||||||
|
|
||||||
|
// 4. 使用 AES 加密大量数据
|
||||||
|
var largeData = "This is a large amount of data that would be inefficient to encrypt with RSA directly...";
|
||||||
|
var encryptedData = CryptoHelper.AesEncrypt(largeData, aesKey);
|
||||||
|
|
||||||
|
// 5. 发送 encryptedAesKey 和 encryptedData...
|
||||||
|
|
||||||
|
// 6. 接收方使用 RSA 私钥解密 AES 密钥
|
||||||
|
var decryptedAesKey = CryptoHelper.RsaDecrypt(encryptedAesKey, privateKey);
|
||||||
|
|
||||||
|
// 7. 使用解密的 AES 密钥解密数据
|
||||||
|
var decryptedData = CryptoHelper.AesDecrypt(encryptedData, decryptedAesKey);
|
||||||
|
|
||||||
|
Assert.AreEqual(aesKey, decryptedAesKey);
|
||||||
|
Assert.AreEqual(largeData, decryptedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTimeExpand 日期时间扩展测试
|
||||||
|
/// 展示日期格式化、时间戳转换、时区转换、日期计算等功能
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class DateTimeExpandTests
|
||||||
|
{
|
||||||
|
#region 日期格式化测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:中文日期格式化
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToChineseDateString_FormatsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 1, 15);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.ToChineseDateString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("2024年01月15日", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:中文日期时间格式化
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToChineseDateTimeString_FormatsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 1, 15, 14, 30, 45);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.ToChineseDateTimeString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("2024年01月15日 14:30:45", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:友好的相对时间字符串
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToFriendlyString_ReturnsCorrectString()
|
||||||
|
{
|
||||||
|
// 测试"刚刚"
|
||||||
|
var justNow = DateTime.Now.AddSeconds(-30);
|
||||||
|
Assert.AreEqual("刚刚", justNow.ToFriendlyString());
|
||||||
|
|
||||||
|
// 测试"分钟前"
|
||||||
|
var minutesAgo = DateTime.Now.AddMinutes(-5);
|
||||||
|
Assert.AreEqual("5分钟前", minutesAgo.ToFriendlyString());
|
||||||
|
|
||||||
|
// 测试"小时前"
|
||||||
|
var hoursAgo = DateTime.Now.AddHours(-3);
|
||||||
|
Assert.AreEqual("3小时前", hoursAgo.ToFriendlyString());
|
||||||
|
|
||||||
|
// 测试"昨天"
|
||||||
|
var yesterday = DateTime.Now.AddDays(-1);
|
||||||
|
Assert.AreEqual("昨天", yesterday.ToFriendlyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:ISO 8601 格式化
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToIso8601String_FormatsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 1, 15, 12, 30, 45, 123, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.ToIso8601String();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("2024-01-15T12:30:45.123Z", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:标准日期格式化
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToDateString_FormatsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.ToDateString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("2024-03-05", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间戳转换测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:转换为 Unix 时间戳(秒)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToUnixTimeSeconds_ConvertsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var timestamp = dt.ToUnixTimeSeconds();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1704067200, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:从 Unix 时间戳创建 DateTime
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void FromUnixTimeSeconds_CreatesCorrectDateTime()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
long timestamp = 1704067200; // 2024-01-01 00:00:00 UTC
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dt = DateTimeExpand.FromUnixTimeSeconds(timestamp);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2024, dt.Year);
|
||||||
|
Assert.AreEqual(1, dt.Month);
|
||||||
|
Assert.AreEqual(1, dt.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:时间戳往返转换
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void UnixTimestamp_RoundTrip_MaintainsValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new DateTime(2024, 6, 15, 12, 30, 45, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var timestamp = original.ToUnixTimeSeconds();
|
||||||
|
var restored = DateTimeExpand.FromUnixTimeSeconds(timestamp);
|
||||||
|
|
||||||
|
// Assert: 精确到秒
|
||||||
|
Assert.AreEqual(original.Year, restored.Year);
|
||||||
|
Assert.AreEqual(original.Month, restored.Month);
|
||||||
|
Assert.AreEqual(original.Day, restored.Day);
|
||||||
|
Assert.AreEqual(original.Hour, restored.Hour);
|
||||||
|
Assert.AreEqual(original.Minute, restored.Minute);
|
||||||
|
Assert.AreEqual(original.Second, restored.Second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:毫秒级时间戳转换
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ToUnixTimeMilliseconds_ConvertsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var timestamp = dt.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1704067200000, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 日期计算测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取月份第一天
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void StartOfMonth_ReturnsFirstDay()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 15, 14, 30, 45);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.StartOfMonth();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2024, result.Year);
|
||||||
|
Assert.AreEqual(3, result.Month);
|
||||||
|
Assert.AreEqual(1, result.Day);
|
||||||
|
Assert.AreEqual(0, result.Hour);
|
||||||
|
Assert.AreEqual(0, result.Minute);
|
||||||
|
Assert.AreEqual(0, result.Second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取月份最后一天
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void EndOfMonth_ReturnsLastDay()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 2, 15); // 2024 是闰年
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.EndOfMonth();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2024, result.Year);
|
||||||
|
Assert.AreEqual(2, result.Month);
|
||||||
|
Assert.AreEqual(29, result.Day); // 闰年 2 月有 29 天
|
||||||
|
Assert.AreEqual(23, result.Hour);
|
||||||
|
Assert.AreEqual(59, result.Minute);
|
||||||
|
Assert.AreEqual(59, result.Second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取年份第一天和最后一天
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void StartOfYear_AndEndOfYear_Work()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 6, 15);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var start = dt.StartOfYear();
|
||||||
|
var end = dt.EndOfYear();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(new DateTime(2024, 1, 1, 0, 0, 0), start);
|
||||||
|
Assert.AreEqual(2024, end.Year);
|
||||||
|
Assert.AreEqual(12, end.Month);
|
||||||
|
Assert.AreEqual(31, end.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取当天开始和结束时间
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void StartOfDay_AndEndOfDay_Work()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 15, 14, 30, 45);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var start = dt.StartOfDay();
|
||||||
|
var end = dt.EndOfDay();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(new DateTime(2024, 3, 15, 0, 0, 0), start);
|
||||||
|
Assert.AreEqual(2024, end.Year);
|
||||||
|
Assert.AreEqual(3, end.Month);
|
||||||
|
Assert.AreEqual(15, end.Day);
|
||||||
|
Assert.AreEqual(23, end.Hour);
|
||||||
|
Assert.AreEqual(59, end.Minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取周的第一天和最后一天
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void StartOfWeek_AndEndOfWeek_Work()
|
||||||
|
{
|
||||||
|
// Arrange: 2024年3月15日是星期五
|
||||||
|
var dt = new DateTime(2024, 3, 15);
|
||||||
|
|
||||||
|
// Act: 周一作为一周的开始
|
||||||
|
var start = dt.StartOfWeek(DayOfWeek.Monday);
|
||||||
|
var end = dt.EndOfWeek(DayOfWeek.Monday);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(DayOfWeek.Monday, start.DayOfWeek);
|
||||||
|
Assert.AreEqual(DayOfWeek.Sunday, end.DayOfWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:添加工作日
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AddWorkDays_SkipsWeekends()
|
||||||
|
{
|
||||||
|
// Arrange: 2024年3月15日是星期五
|
||||||
|
var friday = new DateTime(2024, 3, 15);
|
||||||
|
|
||||||
|
// Act: 添加 1 个工作日应该跳到下周一
|
||||||
|
var nextWorkDay = friday.AddWorkDays(1);
|
||||||
|
|
||||||
|
// Assert: 应该是星期一
|
||||||
|
Assert.AreEqual(DayOfWeek.Monday, nextWorkDay.DayOfWeek);
|
||||||
|
Assert.AreEqual(18, nextWorkDay.Day);
|
||||||
|
|
||||||
|
// Act: 添加 5 个工作日
|
||||||
|
var fiveDaysLater = friday.AddWorkDays(5);
|
||||||
|
|
||||||
|
// Assert: 应该是下周五
|
||||||
|
Assert.AreEqual(22, fiveDaysLater.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:计算工作日数量
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void WorkDaysBetween_CountsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange: 2024年3月11日(周一)到 3月15日(周五)
|
||||||
|
var monday = new DateTime(2024, 3, 11);
|
||||||
|
var friday = new DateTime(2024, 3, 15);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var workDays = monday.WorkDaysBetween(friday);
|
||||||
|
|
||||||
|
// Assert: 周一到周五共 5 个工作日
|
||||||
|
Assert.AreEqual(5, workDays);
|
||||||
|
|
||||||
|
// 测试包含周末的情况
|
||||||
|
var nextMonday = new DateTime(2024, 3, 18);
|
||||||
|
var workDaysWithWeekend = monday.WorkDaysBetween(nextMonday);
|
||||||
|
|
||||||
|
// 周一到下周一:5 + 1 = 6 个工作日(跳过周末)
|
||||||
|
Assert.AreEqual(6, workDaysWithWeekend);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:计算年龄
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void CalculateAge_ComputesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var birthDate = new DateTime(1990, 5, 15);
|
||||||
|
var referenceDate = new DateTime(2024, 6, 20);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var age = birthDate.CalculateAge(referenceDate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(34, age);
|
||||||
|
|
||||||
|
// 测试生日还没到的情况
|
||||||
|
var beforeBirthday = new DateTime(2024, 5, 14);
|
||||||
|
var ageBeforeBirthday = birthDate.CalculateAge(beforeBirthday);
|
||||||
|
|
||||||
|
Assert.AreEqual(33, ageBeforeBirthday);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取季度
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GetQuarter_ReturnsCorrectQuarter()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(1, new DateTime(2024, 1, 1).GetQuarter());
|
||||||
|
Assert.AreEqual(1, new DateTime(2024, 3, 31).GetQuarter());
|
||||||
|
Assert.AreEqual(2, new DateTime(2024, 4, 1).GetQuarter());
|
||||||
|
Assert.AreEqual(2, new DateTime(2024, 6, 30).GetQuarter());
|
||||||
|
Assert.AreEqual(3, new DateTime(2024, 7, 1).GetQuarter());
|
||||||
|
Assert.AreEqual(3, new DateTime(2024, 9, 30).GetQuarter());
|
||||||
|
Assert.AreEqual(4, new DateTime(2024, 10, 1).GetQuarter());
|
||||||
|
Assert.AreEqual(4, new DateTime(2024, 12, 31).GetQuarter());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:获取季度第一天和最后一天
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void StartOfQuarter_AndEndOfQuarter_Work()
|
||||||
|
{
|
||||||
|
// Arrange: 2024年5月15日(第二季度)
|
||||||
|
var dt = new DateTime(2024, 5, 15);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var start = dt.StartOfQuarter();
|
||||||
|
var end = dt.EndOfQuarter();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(new DateTime(2024, 4, 1, 0, 0, 0), start);
|
||||||
|
Assert.AreEqual(6, end.Month);
|
||||||
|
Assert.AreEqual(30, end.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间范围判断测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:判断是否在时间范围内
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void IsBetween_ChecksCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 15);
|
||||||
|
var start = new DateTime(2024, 3, 1);
|
||||||
|
var end = new DateTime(2024, 3, 31);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(dt.IsBetween(start, end));
|
||||||
|
Assert.IsFalse(dt.IsBetween(start, new DateTime(2024, 3, 10)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:判断是否是周末
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void IsWeekend_ChecksCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var saturday = new DateTime(2024, 3, 16); // 周六
|
||||||
|
var sunday = new DateTime(2024, 3, 17); // 周日
|
||||||
|
var monday = new DateTime(2024, 3, 18); // 周一
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(saturday.IsWeekend());
|
||||||
|
Assert.IsTrue(sunday.IsWeekend());
|
||||||
|
Assert.IsFalse(monday.IsWeekend());
|
||||||
|
Assert.IsTrue(monday.IsWorkDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:判断是否是闰年
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void IsLeapYear_ChecksCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var leapYear = new DateTime(2024, 1, 1);
|
||||||
|
var normalYear = new DateTime(2023, 1, 1);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(leapYear.IsLeapYear());
|
||||||
|
Assert.IsFalse(normalYear.IsLeapYear());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:判断是否在过去或未来
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void IsPast_AndIsFuture_Work()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var past = DateTime.Now.AddDays(-1);
|
||||||
|
var future = DateTime.Now.AddDays(1);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(past.IsPast());
|
||||||
|
Assert.IsFalse(past.IsFuture());
|
||||||
|
Assert.IsFalse(future.IsPast());
|
||||||
|
Assert.IsTrue(future.IsFuture());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 时间舍入测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:舍入到小时
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RoundToHour_RemovesMinutesAndSeconds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 15, 14, 30, 45);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.RoundToHour();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(new DateTime(2024, 3, 15, 14, 0, 0), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:舍入到分钟
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RoundToMinute_RemovesSeconds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dt = new DateTime(2024, 3, 15, 14, 30, 45);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = dt.RoundToMinute();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(new DateTime(2024, 3, 15, 14, 30, 0), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试:向上/向下舍入到指定分钟间隔
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void CeilingAndFloorToMinutes_Work()
|
||||||
|
{
|
||||||
|
// Arrange: 14:23
|
||||||
|
var dt = new DateTime(2024, 3, 15, 14, 23, 0);
|
||||||
|
|
||||||
|
// Act: 舍入到 15 分钟间隔
|
||||||
|
var ceiling = dt.CeilingToMinutes(15); // 向上舍入到 14:30
|
||||||
|
var floor = dt.FloorToMinutes(15); // 向下舍入到 14:15
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(30, ceiling.Minute);
|
||||||
|
Assert.AreEqual(15, floor.Minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:生日提醒功能
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_BirthdayReminder()
|
||||||
|
{
|
||||||
|
// Arrange: 用户生日是 1990年5月15日
|
||||||
|
var birthDate = new DateTime(1990, 5, 15);
|
||||||
|
var today = new DateTime(2024, 5, 10);
|
||||||
|
|
||||||
|
// Act: 计算年龄和距离生日的天数
|
||||||
|
var age = birthDate.CalculateAge(today);
|
||||||
|
var thisYearBirthday = new DateTime(today.Year, birthDate.Month, birthDate.Day);
|
||||||
|
var daysUntilBirthday = (thisYearBirthday - today).Days;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(33, age); // 生日还没到,所以是 33 岁
|
||||||
|
Assert.AreEqual(5, daysUntilBirthday);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:工作日计算(项目排期)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_ProjectScheduling()
|
||||||
|
{
|
||||||
|
// Arrange: 项目开始日期是周一
|
||||||
|
var projectStart = new DateTime(2024, 3, 11); // 周一
|
||||||
|
var estimatedWorkDays = 10;
|
||||||
|
|
||||||
|
// Act: 计算项目预计完成日期(只计算工作日)
|
||||||
|
var projectEnd = projectStart.AddWorkDays(estimatedWorkDays - 1);
|
||||||
|
|
||||||
|
// Assert: 10 个工作日后应该是下下周五
|
||||||
|
Assert.AreEqual(DayOfWeek.Friday, projectEnd.DayOfWeek);
|
||||||
|
Assert.AreEqual(22, projectEnd.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:营业时间判断
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_BusinessHoursCheck()
|
||||||
|
{
|
||||||
|
// Arrange: 营业时间是工作日 9:00-18:00
|
||||||
|
var workdayMorning = new DateTime(2024, 3, 15, 10, 30, 0); // 周五 10:30
|
||||||
|
var weekend = new DateTime(2024, 3, 16, 10, 30, 0); // 周六 10:30
|
||||||
|
var afterHours = new DateTime(2024, 3, 15, 19, 30, 0); // 周五 19:30
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.IsTrue(workdayMorning.IsWorkDay());
|
||||||
|
Assert.IsTrue(workdayMorning.Hour >= 9 && workdayMorning.Hour < 18);
|
||||||
|
|
||||||
|
Assert.IsFalse(weekend.IsWorkDay());
|
||||||
|
Assert.IsTrue(afterHours.Hour >= 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:会员到期提醒
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_MembershipExpiration()
|
||||||
|
{
|
||||||
|
// Arrange: 会员到期日期
|
||||||
|
var expirationDate = new DateTime(2024, 12, 31);
|
||||||
|
var today = new DateTime(2024, 12, 20);
|
||||||
|
|
||||||
|
// Act: 计算剩余天数
|
||||||
|
var daysRemaining = (expirationDate - today).Days;
|
||||||
|
var friendlyText = expirationDate.GetTimeSpanFrom(today);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(11, daysRemaining);
|
||||||
|
Assert.AreEqual("11天", friendlyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:活动报名截止时间
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_EventRegistrationDeadline()
|
||||||
|
{
|
||||||
|
// Arrange: 活动时间是 2024年3月20日,报名截止时间是前3个工作日
|
||||||
|
var eventDate = new DateTime(2024, 3, 20); // 周三
|
||||||
|
var workDaysBeforeEvent = 3;
|
||||||
|
|
||||||
|
// Act: 计算报名截止日期
|
||||||
|
var deadline = eventDate.AddWorkDays(-workDaysBeforeEvent).EndOfDay();
|
||||||
|
|
||||||
|
// Assert: 应该是周五(3月15日)的 23:59:59
|
||||||
|
Assert.AreEqual(15, deadline.Day);
|
||||||
|
Assert.AreEqual(23, deadline.Hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景测试:数据统计(按季度)
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_QuarterlyStatistics()
|
||||||
|
{
|
||||||
|
// Arrange: 获取当前季度的起止日期
|
||||||
|
var today = new DateTime(2024, 5, 15);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var quarterStart = today.StartOfQuarter();
|
||||||
|
var quarterEnd = today.EndOfQuarter();
|
||||||
|
var quarter = today.GetQuarter();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, quarter); // 第二季度
|
||||||
|
Assert.AreEqual(new DateTime(2024, 4, 1, 0, 0, 0), quarterStart);
|
||||||
|
Assert.AreEqual(6, quarterEnd.Month);
|
||||||
|
Assert.AreEqual(30, quarterEnd.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
using Common.Utils;
|
||||||
|
|
||||||
|
namespace Common.Test;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ValidationHelper 验证工具测试
|
||||||
|
/// 展示各种数据验证功能和 Result 模式集成
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class ValidationHelperTests
|
||||||
|
{
|
||||||
|
#region 邮箱验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidEmail_ValidEmails_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validEmails = new[]
|
||||||
|
{
|
||||||
|
"test@example.com",
|
||||||
|
"user.name@example.com",
|
||||||
|
"user+tag@example.co.uk",
|
||||||
|
"test123@test-domain.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var email in validEmails)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsValidEmail(email), $"Failed for: {email}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidEmail_InvalidEmails_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidEmails = new[]
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"notanemail",
|
||||||
|
"@example.com",
|
||||||
|
"user@",
|
||||||
|
"user @example.com",
|
||||||
|
"user@.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var email in invalidEmails)
|
||||||
|
{
|
||||||
|
Assert.IsFalse(ValidationHelper.IsValidEmail(email), $"Should fail for: {email}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateEmail_ValidEmail_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var email = "test@example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateEmail(email);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(email, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateEmail_InvalidEmail_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var email = "invalid-email";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateEmail(email);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsFailure);
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("格式无效"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 手机号验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidChinaMobile_ValidMobiles_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validMobiles = new[]
|
||||||
|
{
|
||||||
|
"13812345678",
|
||||||
|
"15912345678",
|
||||||
|
"18612345678",
|
||||||
|
"19912345678"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var mobile in validMobiles)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsValidChinaMobile(mobile), $"Failed for: {mobile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidChinaMobile_InvalidMobiles_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidMobiles = new[]
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"1234567890", // 10 位
|
||||||
|
"123456789012", // 12 位
|
||||||
|
"12812345678", // 第二位是 2
|
||||||
|
"abcdefghijk" // 非数字
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var mobile in invalidMobiles)
|
||||||
|
{
|
||||||
|
Assert.IsFalse(ValidationHelper.IsValidChinaMobile(mobile), $"Should fail for: {mobile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateChinaMobile_ValidMobile_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mobile = "13812345678";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateChinaMobile(mobile);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(mobile, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidInternationalMobile_ValidFormats_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validMobiles = new[]
|
||||||
|
{
|
||||||
|
"+86-13812345678",
|
||||||
|
"+1-5551234567",
|
||||||
|
"+44 7911123456",
|
||||||
|
"+8613812345678"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var mobile in validMobiles)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsValidInternationalMobile(mobile), $"Failed for: {mobile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 身份证验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidChinaIdCard_ValidIdCards_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange: 测试有效的身份证号(包含正确的校验码)
|
||||||
|
var validIdCards = new[]
|
||||||
|
{
|
||||||
|
"11010519491231002X", // 北京,1949年12月31日,校验码 X
|
||||||
|
"110105194912310021" // 北京,1949年12月31日,校验码 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var idCard in validIdCards)
|
||||||
|
{
|
||||||
|
var isValid = ValidationHelper.IsValidChinaIdCard(idCard);
|
||||||
|
// 注意:这里不能保证所有测试身份证都是真实有效的,只是格式和校验码正确
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateChinaIdCard_InvalidFormat_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidIdCards = new[]
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"12345", // 太短
|
||||||
|
"123456789012345678", // 格式不对(日期无效)
|
||||||
|
"11010519491399002X" // 无效日期(99号)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var idCard in invalidIdCards)
|
||||||
|
{
|
||||||
|
var result = ValidationHelper.ValidateChinaIdCard(idCard);
|
||||||
|
Assert.IsTrue(result.IsFailure, $"Should fail for: {idCard}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ExtractBirthDateFromIdCard_ValidIdCard_ReturnsCorrectDate()
|
||||||
|
{
|
||||||
|
// Arrange: 110105199001150011 表示 1990年1月15日
|
||||||
|
var idCard = "110105199001150011";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var birthDate = ValidationHelper.ExtractBirthDateFromIdCard(idCard);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(birthDate);
|
||||||
|
Assert.AreEqual(1990, birthDate.Value.Year);
|
||||||
|
Assert.AreEqual(1, birthDate.Value.Month);
|
||||||
|
Assert.AreEqual(15, birthDate.Value.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ExtractGenderFromIdCard_ValidIdCard_ReturnsCorrectGender()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var maleIdCard = "110105199001150011"; // 倒数第二位是 1(奇数)
|
||||||
|
var femaleIdCard = "110105199001150024"; // 倒数第二位是 2(偶数)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var maleGender = ValidationHelper.ExtractGenderFromIdCard(maleIdCard);
|
||||||
|
var femaleGender = ValidationHelper.ExtractGenderFromIdCard(femaleIdCard);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("Male", maleGender);
|
||||||
|
Assert.AreEqual("Female", femaleGender);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region URL 验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidUrl_ValidUrls_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validUrls = new[]
|
||||||
|
{
|
||||||
|
"http://example.com",
|
||||||
|
"https://www.example.com",
|
||||||
|
"https://example.com/path/to/page?query=value",
|
||||||
|
"http://subdomain.example.com:8080"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var url in validUrls)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsValidUrl(url), $"Failed for: {url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidUrl_InvalidUrls_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidUrls = new[]
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"not-a-url",
|
||||||
|
"ftp://example.com", // 不是 HTTP/HTTPS
|
||||||
|
"//example.com",
|
||||||
|
"example.com" // 缺少协议
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var url in invalidUrls)
|
||||||
|
{
|
||||||
|
Assert.IsFalse(ValidationHelper.IsValidUrl(url), $"Should fail for: {url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateUrl_ValidUrl_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var url = "https://www.example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateUrl(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(url, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 数值范围验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateRange_ValueInRange_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var result = ValidationHelper.ValidateRange(50, 1, 100);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(50, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateRange_ValueOutOfRange_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var tooSmall = ValidationHelper.ValidateRange(0, 1, 100, "年龄");
|
||||||
|
var tooLarge = ValidationHelper.ValidateRange(150, 1, 100, "年龄");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(tooSmall.IsFailure);
|
||||||
|
Assert.IsTrue(tooSmall.ErrorMessage.Contains("年龄"));
|
||||||
|
Assert.IsTrue(tooLarge.IsFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateRange_DoubleValue_Works()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var valid = ValidationHelper.ValidateRange(3.14, 0.0, 10.0, "比率");
|
||||||
|
var invalid = ValidationHelper.ValidateRange(15.0, 0.0, 10.0, "比率");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(valid.IsSuccess);
|
||||||
|
Assert.IsTrue(invalid.IsFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 字符串长度验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateLength_ValidLength_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = "Hello";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateLength(value, 3, 10, "用户名");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(value, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateLength_TooShort_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = "ab";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateLength(value, 3, 10, "用户名");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsFailure);
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("不能少于"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateNotEmpty_EmptyString_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var result1 = ValidationHelper.ValidateNotEmpty("", "用户名");
|
||||||
|
var result2 = ValidationHelper.ValidateNotEmpty(" ", "用户名");
|
||||||
|
var result3 = ValidationHelper.ValidateNotEmpty(null, "用户名");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result1.IsFailure);
|
||||||
|
Assert.IsTrue(result2.IsFailure);
|
||||||
|
Assert.IsTrue(result3.IsFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 密码强度验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsStrongPassword_StrongPassword_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var strongPasswords = new[]
|
||||||
|
{
|
||||||
|
"Abc123!@",
|
||||||
|
"P@ssw0rd",
|
||||||
|
"MySecure123!"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var password in strongPasswords)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsStrongPassword(password), $"Failed for: {password}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsStrongPassword_WeakPassword_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var weakPasswords = new[]
|
||||||
|
{
|
||||||
|
"12345678", // 只有数字
|
||||||
|
"abcdefgh", // 只有小写
|
||||||
|
"ABCDEFGH", // 只有大写
|
||||||
|
"Abc123", // 太短
|
||||||
|
"Abcdefgh", // 缺少数字和特殊字符
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var password in weakPasswords)
|
||||||
|
{
|
||||||
|
Assert.IsFalse(ValidationHelper.IsStrongPassword(password), $"Should fail for: {password}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidatePassword_WeakPassword_ReturnsFailureWithDetails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var password = "weakpass";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidatePassword(password);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsFailure);
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("数字"));
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("大写"));
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("特殊字符"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IP 地址验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidIPv4_ValidIPs_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validIPs = new[]
|
||||||
|
{
|
||||||
|
"192.168.1.1",
|
||||||
|
"10.0.0.1",
|
||||||
|
"172.16.0.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var ip in validIPs)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(ValidationHelper.IsValidIPv4(ip), $"Failed for: {ip}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IsValidIPv4_InvalidIPs_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidIPs = new[]
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"256.1.1.1", // 超出范围
|
||||||
|
"192.168.1", // 不完整
|
||||||
|
"192.168.1.1.1", // 太多段
|
||||||
|
"not.an.ip.address"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var ip in invalidIPs)
|
||||||
|
{
|
||||||
|
Assert.IsFalse(ValidationHelper.IsValidIPv4(ip), $"Should fail for: {ip}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 组合验证测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateAll_AllPass_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var email = "test@example.com";
|
||||||
|
|
||||||
|
// Act: 组合多个验证规则
|
||||||
|
var result = ValidationHelper.ValidateAll(
|
||||||
|
email,
|
||||||
|
e => ValidationHelper.ValidateNotEmpty(e, "邮箱"),
|
||||||
|
e => ValidationHelper.ValidateLength(e, 5, 50, "邮箱"),
|
||||||
|
e => ValidationHelper.ValidateEmail(e)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateAll_OneFails_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var email = "ab"; // 太短
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateAll(
|
||||||
|
email,
|
||||||
|
e => ValidationHelper.ValidateNotEmpty(e, "邮箱"),
|
||||||
|
e => ValidationHelper.ValidateLength(e, 5, 50, "邮箱"),
|
||||||
|
e => ValidationHelper.ValidateEmail(e)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsFailure);
|
||||||
|
Assert.IsTrue(result.ErrorMessage.Contains("长度"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateAny_OnePass_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange: 可以是邮箱或手机号
|
||||||
|
var input = "test@example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateAny(
|
||||||
|
input,
|
||||||
|
i => ValidationHelper.ValidateEmail(i),
|
||||||
|
i => ValidationHelper.ValidateChinaMobile(i)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidateAny_NonPass_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "invalid";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ValidationHelper.ValidateAny(
|
||||||
|
input,
|
||||||
|
i => ValidationHelper.ValidateEmail(i),
|
||||||
|
i => ValidationHelper.ValidateChinaMobile(i)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 实际应用场景测试
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_UserRegistrationValidation()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟用户注册数据
|
||||||
|
var email = "user@example.com";
|
||||||
|
var password = "SecurePass123!";
|
||||||
|
var mobile = "13812345678";
|
||||||
|
|
||||||
|
// Act: 验证所有字段
|
||||||
|
var emailResult = ValidationHelper.ValidateEmail(email);
|
||||||
|
var passwordResult = ValidationHelper.ValidatePassword(password);
|
||||||
|
var mobileResult = ValidationHelper.ValidateChinaMobile(mobile);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(emailResult.IsSuccess);
|
||||||
|
Assert.IsTrue(passwordResult.IsSuccess);
|
||||||
|
Assert.IsTrue(mobileResult.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_FormValidationWithMultipleFields()
|
||||||
|
{
|
||||||
|
// Arrange: 表单数据
|
||||||
|
var formData = new
|
||||||
|
{
|
||||||
|
Email = "invalid-email",
|
||||||
|
Password = "weak",
|
||||||
|
Age = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act: 验证各字段
|
||||||
|
var results = new
|
||||||
|
{
|
||||||
|
Email = ValidationHelper.ValidateEmail(formData.Email),
|
||||||
|
Password = ValidationHelper.ValidatePassword(formData.Password),
|
||||||
|
Age = ValidationHelper.ValidateRange(formData.Age, 0, 150, "年龄")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert: 收集所有错误
|
||||||
|
var errors = new List<string>();
|
||||||
|
if (results.Email.IsFailure)
|
||||||
|
errors.Add(results.Email.ErrorMessage);
|
||||||
|
if (results.Password.IsFailure)
|
||||||
|
errors.Add(results.Password.ErrorMessage);
|
||||||
|
if (results.Age.IsFailure)
|
||||||
|
errors.Add(results.Age.ErrorMessage);
|
||||||
|
|
||||||
|
Assert.AreEqual(3, errors.Count); // 所有字段都有错误
|
||||||
|
Assert.IsTrue(errors[0].Contains("邮箱"));
|
||||||
|
Assert.IsTrue(errors[1].Contains("密码"));
|
||||||
|
Assert.IsTrue(errors[2].Contains("年龄"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Scenario_ChainedValidation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userInput = "test@example.com";
|
||||||
|
|
||||||
|
// Act: 链式验证
|
||||||
|
var result = ValidationHelper.ValidateNotEmpty(userInput, "邮箱")
|
||||||
|
.Bind(e => ValidationHelper.ValidateLength(e, 5, 100, "邮箱"))
|
||||||
|
.Bind(e => ValidationHelper.ValidateEmail(e));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in new issue
Block a user