简体   繁体   中英

EF FromSql() with related entities

Disclaimer: these requirements are not set by me, unless this is an impossible task I cannot convince my boss otherwise.

Let's say we have two entities: Item and ItemTranslation .

public class Item
{
    public int Id { get; set; }

    public string Description { get; set; }

    public virtual ICollection<Item> Children { get; set; }
    public virtual Item Parent { get; set; }
    public virtual ICollection<ItemTranslation> Translations { get; set; }
}

public class ItemTranslation
{
    public int Id { get; set; }

    public string CultureId { get; set; }
    public string Description { get; set; }

    public virtual Item Item { get; set; }
}

The requirement is that Item . Description should be filled in based on a language selected by default, but also allowing it to be specified based on what the user wants. The Item . Description column doesn't actually exist in the database.

In SQL this would be easy: all you have to do is query both tables like so

SELECT [Item].[Id], [ItemTranslation].[Description], [Item].[ParentId] 
FROM [Item] 
LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId]  
WHERE [CultureId] = {cultureId}

Or use an OUTER APPLY depending on your implementation. I have added this query to the .FromSql() function built in Entity Framework.

Put this all together in an OData API and this all works fine for one Item . However as soon as you start using $expand (which behind the scenes is a sort of .Include()) it no longer works. The query being sent to the database for the related entities no longer holds the SQL which I specified in .FromSql() . Only the first query does. On top of this when you would query an Item from a different controller eg ItemTranslation this would also no longer work since .FromSql() is only applied in the other controller.

I could write a query interceptor which simply replaces the generated SQL by Entity Framework and replaces FROM [Item] with FROM [Item] LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId] WHERE [CultureId] = {cultureId} but I wonder if there is a better implementation than that. Perhaps even a redesign in models. I'm open to suggestions.

FromSql has some limitations . I suspect this is the reason why Include won't work.

But once you use EF, why are you messing with SQL? What difficulties does that query have which prevents you from doing it in LINQ? Left join maybe?

from item in ctx.Items
from itemTranslation in ctx.ItemTranslations.Where(it => it.Item.Id == item.Id).DefaultIfEmpty()
where itemTranslation.CultureId == cultureId
select new { item.Id, itemTranslation.Description, ParentId = item.Parent.Id };

Update

Going over the issue again, I see a further problem. Include will only work on an IQueryable<T> where T is an entity whose navigation properties are mapped properly. Now, from this perspective, it doesn't matter if you use FromSql or LINQ if it produces an IQueryable of some projection instead of an entity, Include won't work for obvious reasons.

To be able to include ItemTranslation entities, your action method should look something like this:

[Queryable]
public IQueryable<Item> GetItems()
{
    return db.Items;
}

So the framework can perform $expand on the IQueryable<Item> you return. However, this will include all item translations, not just the ones with the desired culture. If I get it correctly, this is your core issue.

It's obvious as well that you cannot apply this culture filter to an IQueryable<Item> . But you shouldn't do that as this is achieved by $filter in OData:

GET https://.../Items/$expand=Translations&$filter=Translations/CultureId eq culture

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