简体   繁体   中英

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

I'm using Entity Framework Core 5.0 in a ASP.NET Core 3.1 application (this is part of migrating an existing .NET Framework application to .NET Core). I have POCOs defined as such:

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
}

The table schema looks like this (SQL Server 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

According to the Microsoft documentation on owned types , I should be able to use explicit declaration to accomplish this (it's the very first example), so my model builder looks like this (current state; I'm still in the middle of migrating the site):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.RemovePluralizingTableNameConvention(); // https://stackoverflow.com/questions/37493095/entity-framework-core-rc2-table-name-pluralization

    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);
}

All this is hooked to an OData controller:

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(); 
    }
}

The problem is when I hit the OData endpoint with the query string $top=5 , I get an exception saying SqlException: Invalid object name 'MessageDispatcherRoute'. Using SQL Profiler, I can see that EF is trying to join to a non-existent table (note: unrelated columns omitted for brevity).

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

I tried explicitly setting the column names in the model builder, as shown in EF Core 2.2, owned entities generated as another table when multiple in hierarchy , but that changed nothing.

UPDATE: I've made a console application with nothing but a stripped-down DbContext and the 3 POCOs shown above didn't help. If I add [Owned] to the MessageDispatcherRoute class, that results in a really weird output:

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]

What am I doing wrong? Thanks.

The problem might be that you are missing a call to .WithOwner() after calling .OwnsOne() .

From 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.

So, in your case, you could try something like this:

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

You might need to add a migration after this, but I think you don't since your DB schema is already okay.

After just trying random things to see what would happen, the solution was to map the owned type back to the same table, even though the documentation does not give this information at all. In fact, it specifically says this is only required when the owned types are in a separate 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)));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM