Skip to content

Instantly share code, notes, and snippets.

@werwolfby
Last active May 19, 2025 10:27
Show Gist options
  • Save werwolfby/7f04558bc21c8114e209d5727fb2e9f8 to your computer and use it in GitHub Desktop.
Save werwolfby/7f04558bc21c8114e209d5727fb2e9f8 to your computer and use it in GitHub Desktop.
EF Core DateTime Utc kind
public class CustomDbContext : DbContext
{
// ...
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// Replace default materializer source to custom, to convert DateTimes
options.ReplaceService<IEntityMaterializerSource, DateTimeKindEntityMaterializerSource>();
base.OnConfiguring(options);
}
}
public class DateTimeKindEntityMaterializerSource : EntityMaterializerSource
{
private static readonly MethodInfo _normalizeMethod =
typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.Normalize));
private static readonly MethodInfo _normalizeNullableMethod =
typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeNullable));
private static readonly MethodInfo _normalizeObjectMethod =
typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeObject));
public override Expression CreateReadValueExpression(Expression valueBuffer, Type type, int index, IProperty property = null)
{
if (type == typeof(DateTime))
{
return Expression.Call(
_normalizeMethod,
base.CreateReadValueExpression(valueBuffer, type, index, property));
}
if (type == typeof(DateTime?))
{
return Expression.Call(
_normalizeNullableMethod,
base.CreateReadValueExpression(valueBuffer, type, index, property));
}
return base.CreateReadValueExpression(valueBuffer, type, index, property);
}
public override Expression CreateReadValueCallExpression(Expression valueBuffer, int index)
{
var readValueCallExpression = base.CreateReadValueCallExpression(valueBuffer, index);
if (readValueCallExpression.Type == typeof(DateTime))
{
return Expression.Call(
_normalizeMethod,
readValueCallExpression);
}
if (readValueCallExpression.Type == typeof(DateTime?))
{
return Expression.Call(
_normalizeNullableMethod,
readValueCallExpression);
}
if (readValueCallExpression.Type == typeof(object))
{
return Expression.Call(
_normalizeObjectMethod,
readValueCallExpression);
}
return readValueCallExpression;
}
}
public class DateTimeKindMapper
{
public static DateTime Normalize(DateTime value)
=> DateTime.SpecifyKind(value, DateTimeKind.Utc);
public static DateTime? NormalizeNullable(DateTime? value)
=> value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : (DateTime?)null;
public static object NormalizeObject(object value)
=> value is DateTime dateTime ? Normalize(dateTime) : value;
}
@ApacheTech
Copy link

This is the solution I'm using for EFCore in .NET8. It uses mementos for each type to minimise the overhead of reflection for each query.

/// <summary>
///       Intercepts EF Core materialisation and forces all DateTime properties to DateTimeKind.Utc.
///       Uses cached, compiled delegates for maximum performance.
/// </summary>
public class DateTimeKindUtcInterceptor : IMaterializationInterceptor
{
	private static readonly ConcurrentDictionary<Type, Action<object>[]> _materialisers = [];

	/// <inheritdoc/>
	public object InitializedInstance(MaterializationInterceptionData materialisationData, object entity)
	{
		var entityType = entity.GetType();
		var actions = _materialisers.GetOrAdd(entityType, CreateMaterialisersForType);

		foreach (var action in actions)
		{
			action(entity);
		}

		return entity;
	}

	/// <summary>
	///   Builds and caches an array of compiled delegates that set each DateTime/DateTime? property on an instance to UTC.
	/// </summary>
	private static Action<object>[] CreateMaterialisersForType(Type entityType)
	{
		var props = entityType
			.GetProperties(BindingFlags.Public | BindingFlags.Instance)
			.Where(p => p.CanRead && p.CanWrite)
			.Where(p => p.PropertyType == typeof(DateTime)
					 || p.PropertyType == typeof(DateTime?))
			.ToArray();

		var entityParam = Expression.Parameter(typeof(object), "entity");
		var typedEntity = Expression.Convert(entityParam, entityType);

		var actions = new List<Action<object>>(props.Length);

		foreach (var prop in props)
		{
			var propertyExpr = Expression.Property(typedEntity, prop);
			var lambda = prop.PropertyType == typeof(DateTime)
				? CreateDateTimeInterceptor(entityParam, propertyExpr)
				: CreateNullableDateTimeInterceptor(entityParam, propertyExpr);
			actions.Add(lambda);
		}

		return [.. actions];
	}

	private static Action<object> CreateDateTimeInterceptor(ParameterExpression entityParam, MemberExpression propertyExpr)
	{
		var specifyMethod = typeof(DateTime).GetMethod(nameof(DateTime.SpecifyKind), [typeof(DateTime), typeof(DateTimeKind)])!;
		var specifyCall = Expression.Call(specifyMethod, propertyExpr, Expression.Constant(DateTimeKind.Utc));
		var assign = Expression.Assign(propertyExpr, specifyCall);
		var lambda = Expression.Lambda<Action<object>>(assign, entityParam).Compile();
		return lambda;
	}

	private static Action<object> CreateNullableDateTimeInterceptor(ParameterExpression entityParam, MemberExpression propertyExpr)
	{
		var nullableValue = Expression.Property(propertyExpr, "Value");
		var specifyMethod = typeof(DateTime).GetMethod(nameof(DateTime.SpecifyKind), [typeof(DateTime), typeof(DateTimeKind)])!;
		var specifyCall = Expression.Call(specifyMethod, nullableValue, Expression.Constant(DateTimeKind.Utc));
		var nullableAssign = Expression.Assign(propertyExpr, Expression.Convert(specifyCall, typeof(DateTime?)));
		var hasValueCheck = Expression.Property(propertyExpr, "HasValue");
		var conditional = Expression.IfThen(hasValueCheck, nullableAssign);
		var lambda = Expression.Lambda<Action<object>>(conditional, entityParam).Compile();
		return lambda;
	}
}

This interceptor is added as an IInterceptor to the DI.

services.AddSingleton<IInterceptor, DateTimeKindUtcInterceptor>();

And we have a generalised way of adding interceptors with

services.AddDbContextFactory<DbContext>((services, options) =>
{
	options
		.UseSqlServer(...)
		.AddInterceptors(services.GetServices<IInterceptor>());
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment