在Entity Framework Core中,如何将存储过程的结果映射到复杂属性?
我正在尝试将存储过程的结果映射到Entity Framework Core的无键实体,但在运行时遇到了这个异常:
System.InvalidOperationException: The required column 'Info_CreatedByUserId' was not present in the results of a 'FromSql' operation.
这也许只是一个很简单的例子,但我这么做是为了学习,因为我还有另一个存储过程更为复杂,我在映射复杂属性方面陷入了困惑。下面的示例已经被精简到最核心的部分。
尝试#1
首先,这是存储过程:
create or alter procedure my_app_funcs.get_audit_info
(@username varchar(50))
as
begin
select
userProfile.user_id,
userProfile.user_name,
userProfile.create_dt,
userProfile.create_user_id,
userProfile.update_dt,
userProfile.update_user_id
from
my_app.user_profile userProfile
where
lower(userProfile.user_name) = lower(@username);
return;
end
我们曾经在Oracle数据库上工作,但现在已经切换到SQL Server,因此我并没有使用SQL Server/EF的友好命名约定。我目前无法重命名列和表,因为这是一个遗留应用程序。
现在,我要映射到存储过程结果的DTO:
public class UserAuditInfo
{
private UserAuditInfo() { } // for Entity Framework
public UserAuditInfo(string username, AuditInfo info)
{
Username = username;
Info = info;
}
public int Id { get; private set; }
public string Username { get; private set; }
public AuditInfo Info { get; private set; }
}
public class AuditInfo
{
private AuditInfo() { } // for Entity Framework
public AuditInfo(int createdByUserId, DateTime dateCreated, int? updatedByUserId, DateTime? dateUpdated)
{
CreatedByUserId = createdByUserId;
DateCreated = dateCreated;
UpdatedByUserId = updatedByUserId;
DateUpdated = dateUpdated;
}
public int CreatedByUserId { get; private set; }
public DateTime DateCreated { get; private set; }
public int? UpdatedByUserId { get; private set; }
public DateTime? DateUpdated { get; private set; }
}
我们的C#应用程序的命名习惯一直与数据库不同。这个遗留的ASP.NET MVC应用程序使用NHibernate作为ORM,这种不一致在当时并不是问题。我正在启动一个新的Web API项目,想用Entity Framework为我的团队带来一个干净的改造,当然前提是数据库模式不能改;我们只能这样。于是,现在我试图把我们的数据库命名约定与C#的命名约定结合起来。
Entity Framework的映射:
public class SecurityModuleEntities(DbContextOptions options) : DbContext(options)
{
public DbSet<UserAuditInfo> UserAudits { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.HasDefaultSchema("my_app");
builder.Entity<UserAuditInfo>(model =>
{
model.HasNoKey();
model.Property(m => m.Id).HasColumnName("user_id");
model.Property(m => m.Username).HasColumnName("user_name");
model.ComplexProperty(m => m.Info, info =>
{
info.Property(i => i.DateCreated).HasColumnName("create_dt");
info.Property(i => i.CreatedByUserId).HasColumnName("create_user_id");
info.Property(i => i.DateUpdated).HasColumnName("update_dt");
info.Property(i => i.UpdatedByUserId).HasColumnName("update_user_id");
});
});
}
}
最后是我调用存储过程的仓储/仓库:
public class RegisteredUserRepository(SecurityModuleEntities entities) : IRegisteredUserRepository
{
public async Task<IEnumerable<UserAuditInfo>> GetAuditInfo(string username)
{
var sql = FormattableStringFactory.Create("exec my_app_funcs.get_audit_info {0}", username);
var query = entities.UserAudits.FromSqlInterpolated(sql);
var results = await query.ToListAsync(); // exception thrown on this line
// ^^^^^^^^^^^^^
return results;
}
}
对 await query.ToListAsync(); 的调用抛出了本文开头提到的 InvalidOperationException。
当我在SQL Server Management Studio中使用 exec my_app_funcs.get_audit_info 'my-user' 调用此存储过程时,结果如下所示:
| user_id | user_name | create_dt | create_user_id | update_dt | update_user_id |
|---|---|---|---|---|---|
| 262 | my-user | 2017-06-29 09:26:11.000 | 89 | 2026-05-22 16:36:15.013 | 32 |
预期只会得到一行。
开发环境信息:
- .NET 10.0
- Microsoft.EntityFrameworkCore 10.0.0.8
- Microsoft.EntityFrameworkCore.SqlServer 10.0.0.8
- Visual Studio 2026 v18.5.2
我 tried searching 并在微软的Q&A中看到了 this post,但它引用的是基于属性的映射,而不是Fluent映射。这个 other Stack Overflow question 看起来是一样的,但原因是在同一个存储过程里意外多出了两个 SELECT 语句,修复方法是“只做一个SELECT”。我的存储过程里只有一个查询。
尝试#2
我还安装了EF Core Power Tools,并为这次存储过程调用生成了代码。它创建的DTO名称属性直接跟随存储过程列名,没有复杂属性映射,这并不是我要的。我确实改动了代码以调用存储过程,但似乎并没有使用我的映射。我的仓储中的尝试:
public async Task<IEnumerable<UserAuditInfo>> GetAuditInfo(string username)
{
var usernameParam = new SqlParameter("username", username)
{
SqlDbType = System.Data.SqlDbType.VarChar,
Size = 50
};
var results = await entities.SqlQueryAsync<UserAuditInfo>("exec my_app_funcs.get_audit_info @username = @username", [
usernameParam
]);
return results;
}
与原始版本相比,这次尝试唯一的更改是仓储中的方法。DTO、存储过程以及数据映射都保持不变。
上面这段代码在对 await entities.SqlQueryAsync 进行调用时抛出了以下异常:
System.InvalidOperationException: 'The property 'UserAuditInfo.Info' of type 'AuditInfo' appears to be a navigation to another entity type. Navigations are not supported when using 'SqlQuery". Either include this type in the model and use 'FromSql' for the query, or ignore this property using the '[NotMapped]' attribute.'
尝试#3
在这次尝试中,我将类改为直接的DTO——没有构造函数,所有属性都具有公有的get/set。
public class UserAuditInfo
{
public int Id { get; set; }
public string Username { get; set; }
public AuditInfo Info { get; set; }
}
public class AuditInfo
{
public int CreatedByUserId { get; set; }
public DateTime DateCreated { get; set; }
public int? UpdatedByUserId { get; set; }
public DateTime? DateUpdated { get; set; }
}
仓储与实体映射与尝试#1相同。这导致了最初引发本文问题的异常:
System.InvalidOperationException: 'The required column 'Info_CreatedByUserId' was not present in the results of a 'FromSql' operation.'
问题
如何在Entity Framework Core中将 UserAuditInfo.Info 属性从存储过程的结果映射为一个复杂属性?
一个好答案可能是:
- 这里有让它工作的神奇代码。
- 或者“你在和框架较劲,Greg。别这样做。”
解决方案
这不是我的专长领域,但经研究后,我在GitHub的 issue "ComplexProperty column name mapping ignored when loading via FromSql() #34818" 中发现了一个类似的问题报告,该问题被标记为与 "Add View, TVF and SqlQuery mapping support for complex types #34627" 重复。这个问题可能仍然存在(某些人或许比我更懂这件事,能给出更多理由)。
如果确实是未解决的错误,可能的变通办法包括
- 使用别名修改查询结果以匹配预期的命名 -
select ... userProfile.create_user_id as Info_CreatedByUserId ...,或者 - 定义一个简单的类来暂时保存扁平化的查询结果,随后再映射到你偏好的数据结构。