193

I'm doing the mvcmusicstore practice tutorial. I noticed something when creating the scaffold for the album manager (add delete edit).

I want to write code elegantly, so i'm looking for the clean way to write this.

FYI i'm making the store more generic:

Albums = Items

Genres = Categories

Artist = Brand

Here is how the index is retrieved (generated by MVC):

var items = db.Items.Include(i => i.Category).Include(i => i.Brand);

Here is how the item for delete is retrieved:

Item item = db.Items.Find(id);

The first one brings back all the items and populates the category and brand models inside the item model. The second one, doesn't populate the category and brand.

How can i write the second one to do the find AND populate whats inside (preferably in 1 line)... theoretically - something like:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
tereško
  • 58,060
  • 25
  • 98
  • 150
Ralph N
  • 4,240
  • 8
  • 28
  • 36

6 Answers6

207

You can use Include() first, then retrieve a single object from the resulting query:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .FirstOrDefault(x => x.ItemId == id);
Michiel
  • 180
  • 2
  • 14
Dennis Traub
  • 50,557
  • 7
  • 93
  • 108
  • I tried messing around with the order before... but to no sucess. Heres the error i get with your code: 'System.Linq.IQueryable' does not contain a definition for 'Find' and no extension method 'Find' accepting a first argument of type 'System.Linq.IQueryable' could be found (are you missing a using directive or an assembly reference?) – Ralph N Sep 08 '11 at 13:26
  • 2
    Try `...ToList().Find(id)` or `...SingleOrDefault()` instead. See modified answer. – Dennis Traub Sep 08 '11 at 13:28
  • 34
    I would really recommend using the latter (SingleOrDefault), ToList will retrieve *all* entries first and then select one – Sander Rijken Sep 08 '11 at 13:51
  • 7
    This breaks down if we have a composite primary key and are using the relevant find overload. – jhappoldt Sep 23 '11 at 17:01
  • 99
    This would work, but there's a difference between using "Find" and "SingleOrDefault". The "Find" method returns the object from local tracked store if it exists, avoiding a round trip to database, where using "SingleOrDefault" will force a query to database anyway. – Iravanchi Mar 30 '12 at 22:35
  • 3
    @Iravanchi is correct. This may have worked for the user, but the operation and its side-effects are not equivalent to Find, as far as I know. – mwilson Jul 15 '13 at 17:27
  • 1
    @Iravanchi: see my answer below for an alternative of this answer – Learner Mar 28 '14 at 10:10
  • 9
    Does not actually answer the ops question as it is not using .Find – Paul Swetz May 18 '18 at 13:20
  • 7
    Moreover, I would recommend using `FirstOrDefault` instead of `SingleOrDefault`. The first one would generate a `SELECT TOP(1)` while the second would generate a `SELECT TOP(2)` to ensure that there is only one item with the specific predicate. It's not much but there is no valid reason to select more data than necessary except if you really want to ensure that the item you're looking for *must* be unique in the table. – ssougnez Aug 10 '19 at 17:11
102

Dennis' answer is using Include and SingleOrDefault. The latter goes round-tripping to database.

An alternative, is to use Find, in combination with Load, for explicit loading of related entities...

Below an MSDN example:

using (var context = new BloggingContext()) 
{ 
  var post = context.Posts.Find(2); 

  // Load the blog related to a given post 
  context.Entry(post).Reference(p => p.Blog).Load(); 

  // Load the blog related to a given post using a string  
  context.Entry(post).Reference("Blog").Load(); 

  var blog = context.Blogs.Find(1); 

  // Load the posts related to a given blog 
  context.Entry(blog).Collection(p => p.Posts).Load(); 

  // Load the posts related to a given blog  
  // using a string to specify the relationship 
  context.Entry(blog).Collection("Posts").Load(); 
}

Of course, Find returns immediately without making a request to the store, if that entity is already loaded by the context.

Tipx
  • 7,367
  • 4
  • 37
  • 59
Learner
  • 3,297
  • 4
  • 37
  • 62
  • 41
    This method uses `Find` so if the entity is present, there's no round-trip to the DB for the entity itself. BUT, you will have a round-trip for each relationship you're `Load`ing, whereas the `SingleOrDefault` combination with `Include` loads everything in one go. – Iravanchi Mar 31 '14 at 08:20
  • When I compared the 2 in the SQL profiler, Find/Load was better for my case (I had 1:1 relation). @Iravanchi: do you mean to say if I had 1:m relation it would have called m times the store?... because would not make so much sense. – Learner Mar 31 '14 at 08:46
  • 3
    Not 1:m relation, but multiple relationships. Each time you call the `Load` function, the relation should be populated when the call returns. So if you call `Load` multiple times for multiple relations, there will be a round trip each time. Even for a single relation, if the `Find` method does not find the entity in memory, it makes two round trips: one for `Find` and the second for `Load`. But the `Include`.`SingleOrDefault` approach fetches the entity and relation in one go as far as I know (but I'm not sure) – Iravanchi Mar 31 '14 at 13:35
  • 1
    It would have been nice if the could have followed the Include design somehow rather than having to treat collections and references differently. That makes it more difficult to create a GetById() facade that just takes an optional collection of Expression> (e.g. _repo.GetById(id, x => x.MyCollection)) – Derek Greer Jul 17 '15 at 16:20
  • @DerekGreer I am attempting to do just that. Have you come up with a solution? – drizzie Apr 18 '16 at 16:55
  • 5
    Mind to mention the reference of your post: https://msdn.microsoft.com/en-us/data/jj574232.aspx#explicit – Hossein Aug 04 '16 at 09:31
  • Note that there exists a Navigations property which you can call Load on in lieu of going through both the References and Collections properties. – Rick Apr 18 '17 at 15:10
2

There's no real easy way to filter with a find. But I've come up with a close way to replicate the functionality but please take note of a few things for my solution.

This Solutions allows you to filter generically without knowning the primary key in .net-core

  1. Find is fundamentally different because it obtains the the entity if it's present in the tracking before Querying the database.

  2. Additionally It can filter by an Object so the user does not have to know the primary key.

  3. This solution is for EntityFramework Core.

  4. This requires access to the context

Here are some extension methods to add which will help you filter by primary key so

    public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext)
    {
        return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
    }

    //TODO Precompile expression so this doesn't happen everytime
    public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
    {
        var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = keyProperties
            // e => e.PK[i] == id[i]
            .Select((p, i) => Expression.Equal(
                Expression.Property(parameter, p.Name),
                Expression.Convert(
                    Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),
                    p.ClrType)))
            .Aggregate(Expression.AndAlso);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }

    public static Expression<Func<T, object[]>> GetPrimaryKeyExpression<T>(this DbContext context)
    {
        var keyProperties = context.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var keyPropertyAccessExpression = keyProperties.Select((p, i) => Expression.Convert(Expression.Property(parameter, p.Name), typeof(object))).ToArray();
        var selectPrimaryKeyExpressionBody = Expression.NewArrayInit(typeof(object), keyPropertyAccessExpression);

        return Expression.Lambda<Func<T, object[]>>(selectPrimaryKeyExpressionBody, parameter);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id)
        where TEntity : class
    {
        return FilterByPrimaryKey(dbSet.AsQueryable(), context, id);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this IQueryable<TEntity> queryable, DbContext context, object[] id)
        where TEntity : class
    {
        return queryable.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id));
    }

Once you have these extension methods you can filter like so:

query.FilterByPrimaryKey(this._context, id);
johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • 1
    Your solution takes for granded that the primary key is named Id. – vaggelanos Jul 10 '21 at 21:41
  • Expression.Constant(new { id = id[i] }), "id"). You create an anonymous object with a property named id . This get translate to Id = ... . I actually tested it not only read it. – vaggelanos Jul 12 '21 at 13:20
  • @vaggelanos no that, thats a temporary variable in the expression. – johnny 5 Jul 12 '21 at 13:21
  • @vaggelanos this works for composite keys as well. I have this working in my project. – johnny 5 Jul 12 '21 at 13:23
  • look it returns `Expression>` – johnny 5 Jul 12 '21 at 13:23
  • this definitely works in .net-core 2.1. I know because it's working in my project for composite keys. I would further assert that this code was written Not by Me, but by the top answer of .Net-Core issues in SO. My best guess is you've implemented the code wrong. If you link me to a new question I can help answer it for you – johnny 5 Jul 12 '21 at 13:28
  • This is exactly what I needed. Thank you. – Stephen Alex Wallen Feb 11 '22 at 21:38
1

Didnt work for me. But I solved it by doing like this.

var item = db.Items
             .Include(i => i.Category)
             .Include(i => i.Brand)
             .Where(x => x.ItemId == id)
             .First();

Dont know if thats a ok solution. But the other one Dennis gave gave me a bool error in .SingleOrDefault(x => x.ItemId = id);

Nico Schertler
  • 32,049
  • 4
  • 39
  • 70
Johan
  • 27
  • 1
  • 5
    Dennis' solution must work too. Perhaps you have this error in `SingleOrDefault(x => x.ItemId = id)` only because of the wrong single `=` instead of double `==`? – Slauma Sep 10 '11 at 18:00
  • 6
    yeah, looks like you used = not ==. Syntax mistake ;) – Ralph N Sep 13 '11 at 18:29
  • I tried them both == and = still gave me an error in .SingleOrDefault(x => x.ItemId = id); =/ Must be something else in my code that's wrong. But the way I did is a bad way? Maybe I dont understand what you mean Dennis have a singel = in his code aswell. – Johan Sep 18 '11 at 17:57
0

In this scenario you must use DbSet<T>.Local.

You cannot combine DbSet<T>.Find(object[] params) to do what you want because it will query the database if the entity is not currently attached and tracked by the context.

Implementations of DbSet<T>.SingleOrDefault<T>, DbSet<T>.FirstOrDefault<T> and related methods will also query the database immediately upon invocation.

Assuming you have type MyEntity with property Id returning int you could create a method like the following, or adapt it to meet your specific need.

public MyEntity FindLocalOrRemote(int id)
{
    MyEntity entity = 

        context.MyEntities
               .Local
               .SingleOrDefault(p => p.Id == id) 

        ?? 

        context.MyEntities
               .Include(p => p.PackItems)
               .SingleOrDefault(p => p.PackId == id);
        
    return entity;
}

A drawback of this approach, and quite possibly why there is no built-in method for this, might be due to the challenge of designing an API around key values or because using DbSet<T>.Local there is no guarantee that the attached and tracked entity has the related navigation property populated from the database.

This question is really old, but not a single person gave either a simple or correct answer to the question.

This would work with Entity Framework 6 or Entity Framework Core.

David Anderson
  • 13,558
  • 5
  • 50
  • 76
-2

You have to cast IQueryable to DbSet

var dbSet = (DbSet<Item>) db.Set<Item>().Include("");

return dbSet.Find(id);

Lone Ronin
  • 2,530
  • 1
  • 19
  • 31