Why Change the Time?
During development, there are times when you need to change the time for testing.
On Windows, you can usually change the system time through the Control Panel, and the new time takes effect immediately in running programs.
However, you may need to change the time within a program for various reasons, such as company policies preventing system time changes or being unable to remotely access a running server.
To address this, .NET 8 introduced a feature called TimeProvider that lets you abstract time.
What is the TimeProvider class - .NET | Microsoft Learn
But if development is already well underway and the codebase has grown large, you have to find and change every use of DateTime.
Unless you also use static analysis or another method to prevent direct DateTime usage,
the codebase can end up with a mixture of abstracted and direct DateTime calls.
Even if you manage all of that, you still can't abstract the DateTime calls used by third-party libraries.
For these reasons, I needed a way to change the time by hooking DateTime without using .NET's time abstraction.
Hooking with Harmony
Generally, if you want to change a method's behavior 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, you can use a Postfix to modify a method's return value.
The following code adds 100 years to the value returned by 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 doesn't work everywhere.
Whether it worked with DateTime.Now and DateTime.UtcNow depended on the .NET version.
The last time I tested it,
hooking DateTime.UtcNow on .NET 8 threw an exception for reasons I couldn't determine,
while on .NET 7, compiler optimization inlined the method, so the behavior of DateTime.Now didn't change.

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 we begin, this method works only on Windows. It doesn't work on other platforms, such as Linux.
On Windows, .NET uses kernel32.dll to get the time.
The more precise GetSystemTimePreciseAsFileTime would be preferable,
but on misconfigured systems it can produce larger errors, so .NET needs to fall back to GetSystemTimeAsFileTime.
DateTime.Now is drifting away with computer's up time · Issue #9014 · dotnet/runtime
.NET therefore includes code that checks at runtime whether the precise value is reliable and decides whether to fall back.
Performing this check every time would hurt performance, so it chooses one of the two functions and stores its function pointer in a static variable, as shown below.
// .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 replacing this static variable with a pointer to our own time function, we can change the time used throughout .NET without a complicated procedure.
This static variable is both private and readonly, so we can't modify it directly.
However, if we create a method at runtime using DynamicMethod, we can modify an object's fields regardless of these restrictions.
We can therefore use reflection to find s_pfnGetSystemTimeAsFileTime, create a DynamicMethod that accepts a value and sets the field,
and execute it with a pointer to our custom time function as its argument.
The code below implements this approach and accounts for changes in the location of s_pfnGetSystemTimeAsFileTime across .NET versions.
In a multithreaded environment, I recommend calling Init at the start of Main, before starting any threads.
I'm not sure whether compiler optimization is responsible, but in some cases, calling Init after starting threads didn't work reliably in Release builds.
HookedSystemTime Implementation
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}");
}
}
