2

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.

howcheng
  • 2,211
  • 2
  • 17
  • 24

2 Answers2

2

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)));
howcheng
  • 2,211
  • 2
  • 17
  • 24
  • I can't reproduce your issue. – Gert Arnold Jan 29 '21 at 22:07
  • @GertArnold Yeah it doesn't really make much sense. – howcheng Jan 30 '21 at 01:51
  • @howcheng THANK YOU SO MUCH!!!!!! Life saver I wish you many blessings I can't believe that that worked. How annoying it 100% says you don't have to do it if its in one big table but my table was like Address_City column names and it kept trying to join to Address for City instead of Address_City on the main column. That is exactly what you need. I just did a string for the table name and not nameof but same concept thank you!!! – Matthew Young Sep 09 '22 at 19:14
1

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.

dglozano
  • 6,369
  • 2
  • 19
  • 38
  • Unfortunately that had no effect. The generated SQL query is still the same, even with `.WithOwner()`. :'( It's annoying in that this setup wasn't required in EF6. I did forget to mention that this is part of migrating a .NET Framework application to .NET Core. There are no changes in the existing DB schema, but I am using Microsoft's OData for the first time (previously, I had used LinqToQuerystring, which isn't compatible with .NET Core). – howcheng Jan 27 '21 at 17:52
  • @howcheng hmm that's strange. Can't figure out what's wrong. Do you wanna post the rest of your modelbuilder configuration? – dglozano Jan 27 '21 at 18:19
  • There's not much else .. the rest of it is for unrelated tables, but I'll put it up. – howcheng Jan 27 '21 at 18:47
  • I've added the rest of the model builder. Thanks. – howcheng Jan 27 '21 at 20:33
  • @howcheng Hmm yeah that wasn't of much help. Last few things: you haven't defined a `DbSet`, have you? If you have, remove it. You can also try this instead for using the WithOwner: `modelBuilder.Entity< Message >().OwnsOne(x => x. RouteInfo, mdr => mdr.WithOwner());` (same for the other property). That's how I have it configured. And if that doesn't work, you can try to add the `[Owned]` attribute on top of `MessageDispatcherRoute`. If none of that works, then I don't know. – dglozano Jan 27 '21 at 21:18
  • No, there is no `DbSet` defined for that type. Changing the `WithOwner()` bit to be an expression didn't do anything, but adding `[Owned]` made it so that the tables it tries to left join are like `[Message.ReturnRouteInfo#MessageDispatcherRoute]` (WTF). I even made a console application with a stripped-down DbContext and nothing else but the POCOs but there's no difference. – howcheng Jan 27 '21 at 22:50
  • 1
    I finally figured it out. I had to do it like this: `modelBuilder.Entity().OwnsOne(x => x. RouteInfo, mdr => mdr.ToTable("Message"))`. This came as a result of just trying random things to see what would happen. – howcheng Jan 27 '21 at 23:16
  • @howcheng that's weird, the `.ToTable` is used when you want to not use table splitting, and instead store the owned types into their own table. It should by default store them (and query them) from Message. Anyway, I am happy that you managed to make it work :) btw, which provider are you using for EF? – dglozano Jan 28 '21 at 08:40
  • I agree, it's pretty much the opposite of what the documentation says. Anyway, we're using SQL Server. – howcheng Jan 28 '21 at 16:44