The Need to Change Time
During development, there are times when you need to change the time for testing purposes.
In Windows, you can usually change the time through the Control Panel, and the updated time is immediately reflected in running programs.
However, there are various reasons why you might need to change the time within a program, such as company policies that restrict time changes or situations where you cannot remotely access a running server.
To address this, starting from .NET 8, a feature called TimeProvider has been introduced to abstract time handling.
What is the TimeProvider class - .NET | Microsoft Learn
However, if significant development has already taken place and the codebase has grown large, you would need to find and change all instances of DateTime in the code. Unless you prevent the use of the standard DateTime through static analysis or similar means, there could be a mix of abstracted DateTime and regular DateTime in the code.
Moreover, even with such management, there is the issue that you cannot abstract DateTime used in third-party libraries.
For these reasons, there is a need for functionality that allows you to change the time by hooking DateTime without using .NET's time abstraction.
Hooking with Harmony
Typically, when you want to change the behavior of a method at runtime in .NET, you can use a library called Harmony.
GitHub - pardeike/Harmony: A library for patching, replacing and decorating .NET and Mono methods during runtime
For example, using a postfix, you can modify the return value of a method. Below is an example that adds 100 years to the result of DateTime.Now.
using System.Reflection;
using HarmonyLib;
internal class Program
{
static void Main(string[] args)
{
var now = DateTime.Now;
var harmony = new Harmony("TimeHook");
harmony.PatchAll(Assembly.GetExecutingAssembly());
var newNow = DateTime.Now;
Console.WriteLine($"{now}"); // 2025-06-08 5:52:46 PM
Console.WriteLine($"{newNow}"); // 2125-06-08 5:52:46 PM
}
const int YEAR_TEST = 100;
[HarmonyPatch(typeof(DateTime), "get_Now")]
class Patch
{
static DateTime Postfix(DateTime result)
{
return result.AddYears(YEAR_TEST);
}
}
}
However, this approach cannot be applied everywhere. The ability to hook DateTime.Now and DateTime.UtcNow varies depending on the .NET version.
When I last tested it, in the .NET 8 environment, hooking DateTime.UtcNow resulted in an exception for some unknown reason, while in the .NET 7 environment, the method was inlined due to optimization, and the behavior of DateTime.Now was not changed.
Can't override property getter. · Issue #70 · pardeike/Harmony
Failed to patch DateTime.Now with .net 7.0 · Issue #546 · pardeike/Harmony
Hooking with s_pfnGetSystemTimeAsFileTime
Before explaining this method, it is important to note that it only works on Windows and is not applicable on other platforms like Linux.
.NET uses kernel32.dll to retrieve the time on Windows. While it's preferable to use the more precise GetSystemTimePreciseAsFileTime, in poorly configured systems, it can lead to greater errors, so we should fallback to GetSystemTimeAsFileTime.
DateTime.Now is drifting away with computer's up time · Issue #9014 · dotnet/runtime
Thus, .NET includes code at runtime to determine whether there are issues with the Precise value and decide whether to fallback. To avoid performance issues from repeatedly performing this check, the code saves a function pointer to one of the two options in a static variable.
// .NET 7 code
private static readonly unsafe delegate* unmanaged[SuppressGCTransition]<ulong*, void> s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr();
private static unsafe delegate* unmanaged[SuppressGCTransition]<ulong*, void> GetGetSystemTimeAsFileTimeFnPtr()
{
IntPtr kernel32Lib = Interop.Kernel32.LoadLibraryEx(Interop.Libraries.Kernel32, IntPtr.Zero, Interop.Kernel32.LOAD_LIBRARY_SEARCH_SYSTEM32);
Debug.Assert(kernel32Lib != IntPtr.Zero);
IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime");
if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise))
{
for (int i = 0; i < 10; i++)
{
long systemTimeResult, preciseSystemTimeResult;
((delegate* unmanaged[SuppressGCTransition]<long*, void>)pfnGetSystemTime)(&systemTimeResult);
((delegate* unmanaged[SuppressGCTransition]<long*, void>)pfnGetSystemTimePrecise)(&preciseSystemTimeResult);
if (Math.Abs(preciseSystemTimeResult - systemTimeResult) <= 100 * TicksPerMillisecond)
{
pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version
break;
}
}
}
return (delegate* unmanaged[SuppressGCTransition]<ulong*, void>)pfnGetSystemTime;
}
By changing this static variable to our time function, we can change the entire .NET time without going through a complex process.
This static variable is not only private but also readonly, making direct modification impossible. However, when creating a method at runtime using DynamicMethod, it is possible to modify the field of an object regardless of these restrictions.
Thus, we can find s_pfnGetSystemTimeAsFileTime using reflection, create a method with DynamicMethod that takes a parameter to set this field, and execute it with the pointer to our custom time function.
Below is the implementation code that accommodates changes in the location of s_pfnGetSystemTimeAsFileTime depending on the .NET version.
If you are in a multi-threaded environment, it is recommended to perform the initialization in the Main function before the threads are separated. I am not exactly sure if this is due to compiler optimization, but there have been cases where applying the Init after the threads are separated did not work well in Release builds.
Implementation of HookedSystemTime
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
namespace TimeTest;
public static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct FileTime
{
public uint dwLowDateTime;
public uint dwHighDateTime;
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern void GetSystemTimeAsFileTime(out FileTime lpSystemTimeAsFileTime);
}
public unsafe class HookedSystemTime
{
private static int _days;
private static int _hours;
private static int _minutes;
private static int _seconds;
public static bool Initialized { get; private set; }
public static Action? OnChanged;
public static long Ticks
{
get
{
long ticks = 0;
ticks += TimeSpan.TicksPerDay * Volatile.Read(ref _days);
ticks += TimeSpan.TicksPerHour * Volatile.Read(ref _hours);
ticks += TimeSpan.TicksPerMinute * Volatile.Read(ref _minutes);
ticks += TimeSpan.TicksPerSecond * Volatile.Read(ref _seconds);
return ticks;
}
}
public static bool Modified => Initialized && Ticks > 0;
public static void Init()
{
if (Initialized)
return;
ulong testTime;
GetCustomSystemTime(&testTime);
if (testTime <= 0)
return;
IntPtr customFuncPtr = GetCustomSystemTimePtr();
if (customFuncPtr == IntPtr.Zero)
return;
FieldInfo? fieldInfo = null;
foreach (FieldInfo fi in typeof(DateTime).GetRuntimeFields())
{
if (fi.Name == "s_pfnGetSystemTimeAsFileTime" && fi.FieldType == typeof(IntPtr))
{
fieldInfo = fi;
break;
}
}
if (fieldInfo == null)
{
Type? leapClass = typeof(DateTime).GetNestedType("LeapSecondCache", BindingFlags.NonPublic);
if (leapClass != null)
{
foreach (FieldInfo fi in leapClass.GetRuntimeFields())
{
if (fi.Name == "s_pfnGetSystemTimeAsFileTime" && fi.FieldType == typeof(IntPtr))
{
fieldInfo = fi;
break;
}
}
}
}
if (fieldInfo == null)
{
Debug.Fail("Failed to find s_pfnGetSystemTimeAsFileTime field for hooking.");
return;
}
var method = new DynamicMethod(
name: "HookDateTime",
returnType: null,
parameterTypes: new[] { typeof(IntPtr) },
restrictedSkipVisibility: true
);
var gen = method.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Stsfld, fieldInfo);
gen.Emit(OpCodes.Ret);
var fieldSetter = (Action<IntPtr>)method.CreateDelegate(typeof(Action<IntPtr>));
fieldSetter(customFuncPtr);
Initialized = true;
}
private static IntPtr GetCustomSystemTimePtr()
{
var methodInfo = typeof(HookedSystemTime).GetMethod(nameof(GetCustomSystemTime), BindingFlags.Static | BindingFlags.Public);
return methodInfo == null ? IntPtr.Zero : methodInfo.MethodHandle.GetFunctionPointer();
}
public static void GetCustomSystemTime(ulong* ptr)
{
NativeMethods.GetSystemTimeAsFileTime(out NativeMethods.FileTime fileTime);
ulong combined = ((ulong)fileTime.dwHighDateTime << 32) | fileTime.dwLowDateTime;
combined += (ulong)Ticks;
*ptr = combined;
}
public static void AddSeconds(int seconds)
{
AddOffset(0, 0, 0, seconds);
}
public static void AddMinutes(int minutes)
{
AddOffset(0, 0, minutes, 0);
}
public static void AddHours(int hours)
{
AddOffset(0, hours, 0, 0);
}
public static void AddDays(int days)
{
AddOffset(days, 0, 0, 0);
}
public static void AddOffset(int days, int hours, int minutes, int seconds)
{
if (!Initialized)
return;
if (days == 0 && hours == 0 && minutes == 0 && seconds == 0)
return;
if (days != 0) Interlocked.Add(ref _days, days);
if (hours != 0) Interlocked.Add(ref _hours, hours);
if (minutes != 0) Interlocked.Add(ref _minutes, minutes);
if (seconds != 0) Interlocked.Add(ref _seconds, seconds);
OnChanged?.Invoke();
}
public static DateTime GetOriginalNow()
{
return Modified ? DateTime.Now.AddTicks(-Ticks) : DateTime.Now;
}
}
Test Code
You can test it as follows:
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"[Main] Before: {DateTime.Now}");
for (int i = 0; i < 4; i++)
{
int index = i;
new Thread(() =>
{
Console.WriteLine($"[Thread {index}] Before: {DateTime.Now}");
Thread.Sleep(3000);
Console.WriteLine($"[Thread {index}] After: {DateTime.Now}");
}).Start();
}
HookedSystemTime.Init();
HookedSystemTime.AddDays(100);
Console.WriteLine($"[Main] After: {DateTime.Now}");
}
}