diff --git a/src/WalkingTec.Mvvm.Core/Dashboard/JsonFileDashboardService.cs b/src/WalkingTec.Mvvm.Core/Dashboard/JsonFileDashboardService.cs index 009f89236..f0185857a 100644 --- a/src/WalkingTec.Mvvm.Core/Dashboard/JsonFileDashboardService.cs +++ b/src/WalkingTec.Mvvm.Core/Dashboard/JsonFileDashboardService.cs @@ -17,6 +17,7 @@ public class JsonFileDashboardService : IDashboardService private readonly DashboardOptions _options; private readonly IEnumerable _dataSources; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _index = new(); private readonly ConcurrentDictionary _locks = new(); private readonly string _baseDir; @@ -26,11 +27,13 @@ public class JsonFileDashboardService : IDashboardService public JsonFileDashboardService( IOptions options, IEnumerable dataSources, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _options = options.Value; _dataSources = dataSources; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _options.DashboardDirectory); logger.LogWarning( @@ -184,7 +187,7 @@ public async Task CreateAsync(DashboardDefinition dashboard) dashboard.Id = Guid.NewGuid().ToString("N"); } - dashboard.CreatedAt = DateTime.UtcNow; + dashboard.CreatedAt = _timeProvider.GetUtcNow().UtcDateTime; dashboard.UpdatedAt = dashboard.CreatedAt; var path = GetFilePath(dashboard.Id, dashboard.TenantId); @@ -241,7 +244,7 @@ public async Task UpdateAsync(DashboardDefinition dashboard) throw new ArgumentException("Dashboard ID cannot be null or empty.", nameof(dashboard)); } - dashboard.UpdatedAt = DateTime.UtcNow; + dashboard.UpdatedAt = _timeProvider.GetUtcNow().UtcDateTime; var path = GetFilePath(dashboard.Id, dashboard.TenantId); var lockObj = GetLock(dashboard.Id); diff --git a/src/WalkingTec.Mvvm.Core/DataContext.cs b/src/WalkingTec.Mvvm.Core/DataContext.cs index 02a94f083..4642c18b6 100644 --- a/src/WalkingTec.Mvvm.Core/DataContext.cs +++ b/src/WalkingTec.Mvvm.Core/DataContext.cs @@ -448,6 +448,11 @@ public string? TenantCode public DBTypeEnum DBType { get; set; } + /// + /// 可測試的時間來源。預設 TimeProvider.System。 + /// + public TimeProvider TimeProvider { get; set; } = TimeProvider.System; + public string? Version { get; set; } public CS ConnectionString { get; set; } = null!; public DbSet AnalysisSavedQueries { get; set; } = null!; @@ -879,14 +884,14 @@ private void ApplyAuditFields() { case EntityState.Added: if (entity.CreateTime == null) - entity.CreateTime = DateTime.Now; + entity.CreateTime = TimeProvider.GetLocalNow().DateTime; if (string.IsNullOrEmpty(entity.CreateBy)) entity.CreateBy = CurrentUserCode; break; case EntityState.Modified: if (entity.UpdateTime == null) - entity.UpdateTime = DateTime.Now; + entity.UpdateTime = TimeProvider.GetLocalNow().DateTime; if (string.IsNullOrEmpty(entity.UpdateBy)) entity.UpdateBy = CurrentUserCode; break; diff --git a/src/WalkingTec.Mvvm.Core/Support/DateRange.cs b/src/WalkingTec.Mvvm.Core/Support/DateRange.cs index 93c462779..7e8261e4b 100644 --- a/src/WalkingTec.Mvvm.Core/Support/DateRange.cs +++ b/src/WalkingTec.Mvvm.Core/Support/DateRange.cs @@ -276,59 +276,52 @@ public static DateRange Yesterday public static DateRange UtcDefault => UtcToday; - public static DateRange UtcNinetyDays + /// UTC 過去 90 天。 + public static DateRange UtcNinetyDays => CreateUtcNinetyDays(); + public static DateRange CreateUtcNinetyDays(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-90), DateTime.UtcNow.Date); - return result; - } + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-90), today); } - public static DateRange UtcThirtyDays + /// UTC 過去 30 天。 + public static DateRange UtcThirtyDays => CreateUtcThirtyDays(); + public static DateRange CreateUtcThirtyDays(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-30), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); - return result; - } + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-30), today, DefaultType, UtCDefaultEpoch); } - public static DateRange UtcTwoWeek + /// UTC 過去 14 天。 + public static DateRange UtcTwoWeek => CreateUtcTwoWeek(); + public static DateRange CreateUtcTwoWeek(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-14), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); - return result; - } + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-14), today, DefaultType, UtCDefaultEpoch); } - public static DateRange UtcWeek + /// UTC 過去 7 天。 + public static DateRange UtcWeek => CreateUtcWeek(); + public static DateRange CreateUtcWeek(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-7), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); - return result; - } - + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-7), today, DefaultType, UtCDefaultEpoch); } - public static DateRange UtcToday + /// UTC 昨天到今天。 + public static DateRange UtcToday => CreateUtcToday(); + public static DateRange CreateUtcToday(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-1), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); - return result; - } + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-1), today, DefaultType, UtCDefaultEpoch); } - public static DateRange UtcYesterday + /// UTC 昨天。 + public static DateRange UtcYesterday => CreateUtcYesterday(); + public static DateRange CreateUtcYesterday(TimeProvider? timeProvider = null) { - get - { - var result = new DateRange(DateTime.UtcNow.Date.AddDays(-1), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); - return result; - } + var today = (timeProvider ?? TimeProvider.System).GetUtcNow().UtcDateTime.Date; + return new DateRange(today.AddDays(-1), today, DefaultType, UtCDefaultEpoch); } diff --git a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs index 6985b622d..1374c815a 100644 --- a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs +++ b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs @@ -35,7 +35,7 @@ public IWtmFile UploadToDB(string fileName, long fileLength, Stream data, strin FileAttachment file = new FileAttachment(); file.FileName = fileName; file.Length = fileLength; - file.UploadTime = DateTime.Now; + file.UploadTime = wtm.TimeProvider.GetLocalNow().DateTime; file.SaveMode = _modeName; file.ExtraInfo = extra; file.TenantCode = wtm.LoginUserInfo?.CurrentTenant; diff --git a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs index 9cdda2a5d..dd94313c8 100644 --- a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs +++ b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs @@ -113,7 +113,7 @@ public IWtmFileHandler CreateFileHandler(string? saveMode = null, IDataContext? FileAttachment file = new FileAttachment(); file.FileName = fileName; file.Length = fileLength; - file.UploadTime = DateTime.Now; + file.UploadTime = _wtm.TimeProvider.GetLocalNow().DateTime; file.SaveMode = string.IsNullOrEmpty(saveMode) == true ? _wtm.ConfigInfo.FileUploadOptions.SaveFileMode! : saveMode; file.ExtraInfo = extra; var ext = string.Empty; diff --git a/src/WalkingTec.Mvvm.Core/Support/WTMLogger.cs b/src/WalkingTec.Mvvm.Core/Support/WTMLogger.cs index 02d4c19a2..dd9150078 100644 --- a/src/WalkingTec.Mvvm.Core/Support/WTMLogger.cs +++ b/src/WalkingTec.Mvvm.Core/Support/WTMLogger.cs @@ -100,11 +100,12 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except ll = ActionLogTypesEnum.Exception; } + var now = (sp.GetService() ?? TimeProvider.System).GetLocalNow().DateTime; log = new ActionLog { Remark = formatter?.Invoke(state, exception), - CreateTime = DateTime.Now, - ActionTime = DateTime.Now, + CreateTime = now, + ActionTime = now, ActionName = "WtmLog", ModuleName = "WtmLog", LogType = ll diff --git a/src/WalkingTec.Mvvm.Etl/Scheduling/EtlQuartzJob.cs b/src/WalkingTec.Mvvm.Etl/Scheduling/EtlQuartzJob.cs index 252a9b234..8cbe1e2e4 100644 --- a/src/WalkingTec.Mvvm.Etl/Scheduling/EtlQuartzJob.cs +++ b/src/WalkingTec.Mvvm.Etl/Scheduling/EtlQuartzJob.cs @@ -54,8 +54,8 @@ public override async Task Execute(IJobExecutionContext context) JobId = jobDefId, Trigger = trigger, Result = EtlRunResult.Skipped, - StartedAt = DateTime.UtcNow, - FinishedAt = DateTime.UtcNow + StartedAt = Wtm.TimeProvider.GetUtcNow().UtcDateTime, + FinishedAt = Wtm.TimeProvider.GetUtcNow().UtcDateTime }); await dc.SaveChangesAsync(); @@ -71,7 +71,7 @@ public override async Task Execute(IJobExecutionContext context) dc.Set().Update(jobDef); await dc.SaveChangesAsync(); - var startedAt = DateTime.UtcNow; + var startedAt = Wtm.TimeProvider.GetUtcNow().UtcDateTime; // 建立超時 CancellationToken(連結 Quartz 的 CancellationToken 以支援中止) using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken); @@ -140,7 +140,7 @@ public override async Task Execute(IJobExecutionContext context) if (result.Success) { jobDef.LastWatermarkValue = result.NewWatermarkValue; - jobDef.LastRunAt = DateTime.UtcNow; + jobDef.LastRunAt = Wtm.TimeProvider.GetUtcNow().UtcDateTime; jobDef.LastError = null; jobDef.Status = EtlJobStatus.Enabled; jobDef.ConsecutiveFailureCount = 0; // reset on success @@ -164,7 +164,7 @@ public override async Task Execute(IJobExecutionContext context) { Success = false, ErrorMessage = sanitized, - ElapsedMs = (long)(DateTime.UtcNow - startedAt).TotalMilliseconds + ElapsedMs = (long)(Wtm.TimeProvider.GetUtcNow().UtcDateTime - startedAt).TotalMilliseconds }; var currentAttempt = context.MergedJobDataMap.ContainsKey("_retryAttempt") @@ -207,7 +207,7 @@ public override async Task Execute(IJobExecutionContext context) ElapsedMs = result?.ElapsedMs ?? 0, ErrorMessage = result?.ErrorMessage, StartedAt = startedAt, - FinishedAt = DateTime.UtcNow, + FinishedAt = Wtm.TimeProvider.GetUtcNow().UtcDateTime, WatermarkSnapshot = jobDef.LastWatermarkValue }; dc.Set().Add(runLog); diff --git a/src/WalkingTec.Mvvm.Etl/Scheduling/EtlSchedulerService.cs b/src/WalkingTec.Mvvm.Etl/Scheduling/EtlSchedulerService.cs index 09ac72d61..697252b79 100644 --- a/src/WalkingTec.Mvvm.Etl/Scheduling/EtlSchedulerService.cs +++ b/src/WalkingTec.Mvvm.Etl/Scheduling/EtlSchedulerService.cs @@ -42,7 +42,7 @@ public async Task ResetGhostRunningJobsAsync() if (ghostJobs.Count == 0) return; - var now = DateTime.UtcNow; + var now = (_sp.GetService() ?? TimeProvider.System).GetUtcNow().UtcDateTime; foreach (var job in ghostJobs) { job.Status = EtlJobStatus.Failed; diff --git a/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs b/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs index be702fff4..1e2e2e707 100644 --- a/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs +++ b/src/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs @@ -377,7 +377,7 @@ public override void OnResultExecuted(ResultExecutedContext context) var postDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(HttpPostAttribute), false).Cast().FirstOrDefault(); log.LogType = context.Exception == null ? ActionLogTypesEnum.Normal : ActionLogTypesEnum.Exception; - log.ActionTime = ctrl.Wtm?.TimeProvider.GetLocalNow().DateTime ?? DateTime.Now; + log.ActionTime = (ctrl.Wtm?.TimeProvider ?? TimeProvider.System).GetLocalNow().DateTime; log.ITCode = ctrl.Wtm?.LoginUserInfo?.ITCode ?? string.Empty; // 给日志的多语言属性赋值 log.ModuleName = ctrlDes?.GetDescription(ctrl) ?? ctrlActDesc.ControllerName; @@ -392,7 +392,7 @@ public override void OnResultExecuted(ResultExecutedContext context) var starttime = context.HttpContext.Items["actionstarttime"] as DateTime?; if (starttime != null) { - log.Duration = (ctrl.Wtm?.TimeProvider.GetLocalNow().DateTime ?? DateTime.Now).Subtract(starttime.Value).TotalSeconds; + log.Duration = (ctrl.Wtm?.TimeProvider ?? TimeProvider.System).GetLocalNow().DateTime.Subtract(starttime.Value).TotalSeconds; } try {