如何在C#/EF Core中Mock AddMilliseconds()或(object)类型转换?
问题背景
我有一个SQL Server数据库,表结构如下:
StartDate: date (可为null) EndDate: date (可为null) PreferredFromMs : bigint (可为null) PreferredToMs : bigint (可为null)
开始/结束日期时间在SQL Server端计算:日期部分(date类型)加上以毫秒存储的时间部分(bigint类型),二者分开保存。
该查询在EF Core 7应用中运行正常,但单元测试Mock时抛出异常:
System.InvalidCastException: 'Unable to cast object of type 'System.TimeSpan' to type 'System.Int64'
查询代码如下:
var data = query .Where(r => routeStart <= ((DateTime)(object)r.EndDate.Value).AddMilliseconds(r.PreferredToMs == null ? 0 : (long)(object)r.PreferredToMs.Value) && routeEnd >= ((DateTime)(object)r.StartDate.Value).AddMilliseconds(r.PreferredFromMs == null ? 0 : (long)(object)r.PreferredFromMs.Value)) .ToList();
这些(object)类型转换看似不合理,但却是必要的:
- EndDate是
date类型,无时间部分。如果不转换就调用AddMilliseconds(),Linq-to-entities无法正确转换表达式(需要datetime而非date类型) - PreferredToMs是
bigint类型,无法直接作为TimeSpan使用,否则表达式会被转换为DATEPART(millisecond, [r].[PreferredToMs]) AS bigint),而DATEPART()无法处理bigint,会导致:
Arithmetic overflow error converting expression to data type datetime
我的Mock设置如下:
IQueryable<Data> _fakeData = new List<Data>(1) { _myData }.AsQueryable();
mock.Mock<IMyRepository>().Setup(x => x.Query()).Returns(_fakeData);
请问如何正确处理这些(object)或(long)类型转换?或者是否应该Mock AddMilliseconds()方法?
解决方案
方案1:重构查询逻辑,避免强制类型转换
把日期和时间的拼接逻辑封装成计算属性或数据库标量函数映射,既能让EF Core正确转换,也能让内存Mock数据正常运行。
方式A:实体类添加计算属性(配合EF Core映射)
在Data实体类中添加两个计算属性:
public DateTime? FullStartDate => StartDate.HasValue ? StartDate.Value.AddMilliseconds(PreferredFromMs ?? 0) : null; public DateTime? FullEndDate => EndDate.HasValue ? EndDate.Value.AddMilliseconds(PreferredToMs ?? 0) : null;
修改查询逻辑:
var data = query .Where(r => routeStart <= r.FullEndDate && routeEnd >= r.FullStartDate) .ToList();
如果要让EF Core将该属性映射为数据库计算列,需在上下文配置中添加:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Data>() .Property(d => d.FullStartDate) .HasComputedColumnSql("CAST([StartDate] AS DATETIME) + DATEADD(MILLISECOND, ISNULL([PreferredFromMs], 0), 0)"); modelBuilder.Entity<Data>() .Property(d => d.FullEndDate) .HasComputedColumnSql("CAST([EndDate] AS DATETIME) + DATEADD(MILLISECOND, ISNULL([PreferredToMs], 0), 0)"); }
这样内存Mock数据会直接使用计算属性逻辑,不会出现类型转换异常。
方式B:映射SQL Server标量函数
创建SQL Server标量函数:
CREATE FUNCTION dbo.GetFullDateTime(@date DATE, @ms BIGINT) RETURNS DATETIME AS BEGIN RETURN CAST(@date AS DATETIME) + DATEADD(MILLISECOND, ISNULL(@ms, 0), 0) END
在EF Core中映射该函数:
public static class DbFunctionsExtensions { [DbFunction("GetFullDateTime", "dbo")] public static DateTime GetFullDateTime(DateTime date, long? ms) { return date.AddMilliseconds(ms ?? 0); } }
修改查询:
var data = query .Where(r => routeStart <= DbFunctionsExtensions.GetFullDateTime(r.EndDate.Value, r.PreferredToMs) && routeEnd >= DbFunctionsExtensions.GetFullDateTime(r.StartDate.Value, r.PreferredFromMs)) .ToList();
这种方式下,数据库查询会调用SQL函数,内存Mock查询会执行C#函数实现,两边逻辑一致。
方案2:改用EF Core内存数据库代替Mock
直接使用EF Core内存数据库模拟真实查询场景,避免Mock带来的表达式树差异:
var options = new DbContextOptionsBuilder<MyDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .Options; using var context = new MyDbContext(options); context.Data.Add(_myData); context.SaveChanges(); var repository = new MyRepository(context); var data = repository.Query() .Where(r => routeStart <= ((DateTime)(object)r.EndDate.Value).AddMilliseconds(r.PreferredToMs == null ? 0 : (long)(object)r.PreferredToMs.Value) && routeEnd >= ((DateTime)(object)r.StartDate.Value).AddMilliseconds(r.PreferredFromMs == null ? 0 : (long)(object)r.PreferredFromMs.Value)) .ToList();
这种方式最贴近真实运行环境,完全规避Mock时的类型转换问题。
方案3:调整Mock数据的类型处理
如果不想重构查询,需确保Mock数据的类型匹配:
- 确保
_myData中的EndDate/StartDate是有效的DateTime(EF Core中date类型映射为DateTime,时间部分默认0) - 确保
PreferredFromMs/PreferredToMs为long?类型,避免装箱后的类型转换异常
也可以使用EntityFrameworkCore.Testing这类专门的EF Core测试库,它能更好地处理EF Core表达式转换逻辑,减少内存与数据库查询的差异。
内容的提问来源于stack exchange,提问作者T Sl

