Last active
May 19, 2025 10:27
-
-
Save werwolfby/7f04558bc21c8114e209d5727fb2e9f8 to your computer and use it in GitHub Desktop.
EF Core DateTime Utc kind
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
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
Can you make an example using .Net 6?