Entity Framework Core 在使用自有类型时尝试加入不存在的 table

Entity Framework Core tries to join to a non-existent table when using an owned type

我在 ASP.NET Core 3.1 应用程序中使用 Entity Framework Core 5.0(这是将现有 .NET Framework 应用程序迁移到 .NET Core 的一部分)。我将 POCO 定义为:

public class Message
{
    public Guid MessageId { get; set; }
    public MessageDispatcherRoute RouteInfo { get; set; }
    public MessageDispatcherRoute ReturnRouteInfo { get; set; }
    public List<MessageResponse> Responses { get; set; }
    // other properties
}

public class MessageDispatcherRoute
{
    public string Url { get; set; }
    public string HttpVerb { get; set; }
}

public class MessageResponse
{
    public Guid MessageId { get; set; }
    public int ResponseNumber { get; set; }
    // other properties
}

table 架构如下所示(SQL 服务器 13.0):

CREATE TABLE [Message] (
    [MessageId] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    [RouteInfo_Url] VARCHAR(255),
    [RouteInfo_HttpVerb] VARCHAR(10),
    [ReturnRouteInfo_Url] VARCHAR(255),
    [ReturnRouteInfo_HttpVerb] VARCHAR(10),
    -- other columns
}
-- MessageResponse is a separate table

根据 Microsoft documentation on owned types,我应该能够使用显式声明来完成此操作(这是第一个示例),所以我的模型构建器看起来像这样(当前状态;我仍处于迁移网站的中间):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.RemovePluralizingTableNameConvention(); // 

    modelBuilder.Entity<App>().HasKey(x => x.AppId);
    modelBuilder.Entity<App>().HasMany(x => x.AppSettings);
    modelBuilder.Entity<CustomQueue>().HasKey(x => x.CustomQueueId);
    modelBuilder.Entity<Message>().HasKey(m => m.MessageId);
    modelBuilder.Entity<Message>().OwnsOne(m => m.RouteInfo);
    modelBuilder.Entity<Message>().OwnsOne(m => m.ReturnRouteInfo);
    modelBuilder.Entity<MessageResponse>().HasKey(m => new { m.MessageId, m.ResponseNumber });
    modelBuilder.Entity<Message>().HasMany(m => m.Responses);
}

所有这些都挂接到 OData 控制器:

public class MessagesController : Controller
{
    private readonly IMyContext _context;

    public MessagesController(IMyContext context)
    {
        _context = context;
    }

    [HttpGet]
    [EnableQuery(MaxExpansionDepth = 1)] 
    public IQueryable<Message> Get(ODataQueryOptions options)
    {
        return _context.Messages.AsQueryable().Include(x => x.Responses).AsSingleQuery(); 
    }
}

问题是当我使用查询字符串 $top=5 访问 OData 端点时,我得到一个异常提示 SqlException: Invalid object name 'MessageDispatcherRoute'. 使用 SQL Profiler,我可以看到 EF 正在尝试加入一个不存在的 table(注意:为简洁起见省略了不相关的列)。

exec sp_executesql N'SELECT [t].[MessageId] 
, [m1].[MessageId], [m1].[RouteInfo_HttpVerb], [m1].[RouteInfo_Url]
, [m0].[MessageId], [m2].[MessageId], [m2].[ResponseNumber]
FROM (
    SELECT TOP(@__TypedProperty_0) [m].[MessageId]
    , [m].[ReturnRouteInfo_HttpVerb], [m].[ReturnRouteInfo_Url]
    FROM [Message] AS [m]
    ORDER BY [m].[MessageId]
) AS [t]
LEFT JOIN [MessageDispatcherRoute] AS [m0] ON [t].[MessageId] = [m0].[MessageId]
LEFT JOIN [MessageDispatcherRoute] AS [m1] ON [t].[MessageId] = [m1].[MessageId]
LEFT JOIN [MessageResponse] AS [m2] ON [t].[MessageId] = [m2].[MessageId]
ORDER BY [t].[MessageId], [m0].[MessageId], [m1].[MessageId], [m2].[MessageId], [m2].[ResponseNumber]'
,N'@__TypedProperty_0 int',@__TypedProperty_0=5

我尝试在模型构建器中显式设置列名称,如 所示,但没有任何改变。

更新:我制作了一个控制台应用程序,除了一个精简的 DbContext 之外什么都没有,上面显示的 3 个 POCO 没有帮助。如果我将 [Owned] 添加到 MessageDispatcherRoute class,那会导致非常奇怪的输出:

LEFT JOIN [Message.ReturnRouteInfo#MessageDispatcherRoute] AS [m0] ON [t].[MessageId] = [m0].[MessageID]
LEFT JOIN [Message.RouteInfo#MessageDispatcherRoute] AS [m1] ON [t].[MessageId] = [m1].[MessageID]
LEFT JOIN [Message.ReturnRouteInfo#MessageDispatcherRoute] AS [m2] ON [t].[MessageId] = [m2].[MessageID]

我做错了什么?谢谢。

问题可能是您在调用 .OwnsOne() 后错过了对 .WithOwner() 的调用。

来自the docs

Configures a relationship where the target entity is owned by (or part of) this entity.

The target entity type for each ownership relationship is treated as a different entity type even if the navigation is of the same type. Configuration of the target entity type isn't applied to the target entity type of other ownership relationships.

Most operations on an owned entity require accessing it through the owner entity using the corresponding navigation.

After calling this method, you should chain a call to WithOwner(String) to fully configure the relationship.

所以,在你的情况下,你可以尝试这样的事情:

modelBuilder.Entity<Message>().OwnsOne(m => m.RouteInfo).WithOwner();
modelBuilder.Entity<Message>().OwnsOne(m => m.ReturnRouteInfo).WithOwner();

您可能需要在此之后添加迁移,但我认为您不需要,因为您的数据库架构已经没问题了。

在随机尝试看看会发生什么之后,解决方案是将拥有的类型映射回相同的 table,即使 the documentation 根本没有提供此信息。事实上,它明确表示仅当拥有的类型位于单独的 table.

中时才需要这样做
modelBuilder.Entity<Message>().OwnsOne(m => m.RouteInfo, mdr => mdr.ToTable(nameof(Message)));
modelBuilder.Entity<Message>().OwnsOne(m => m.ReturnRouteInfo, mdr => mdr.ToTable(nameof(Message)));