Lyudmil Dimitrov
Lyudmil Dimitrov

Reputation: 491

DbSet doesn't have a Find method in EF7

I am trying to create a generic repository to access my database. In EF6 I was able to do that in order to get a specific entity:

protected IDbSet<T> dbset;

public T Get(object id)
{
    return this.dbset.Find(id);
}

DbSet in EF7 is missing a Find method. Is there a way to implement the above piece of code?

Upvotes: 47

Views: 18621

Answers (11)

sjb-sjb
sjb-sjb

Reputation: 1197

Let me contribute a revision that includes building the expression. I'll confess I didn't actually test this ;-)

    public static TEntity Find<TEntity>(this DbSet<TEntity> dbSet, params object[] keyValues) where TEntity : class
    {
        // Find DbContext, entity type, and primary key.
        var context = ((IInfrastructure<IServiceProvider>)dbSet).GetService<DbContext>();
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        var key = entityType.FindPrimaryKey();

        // Build the lambda expression for the query: (TEntity entity) => AND( entity.keyProperty[i] == keyValues[i])
        var entityParameter = Expression.Parameter(typeof(TEntity), "entity");
        Expression whereClause = Expression.Constant(true, typeof(bool));
        uint i = 0;

        foreach (var keyProperty in key.Properties) {
            var keyMatch = Expression.Equal(
                Expression.Property(entityParameter, keyProperty.Name),
                Expression.Constant(keyValues[i++])
            );

            whereClause = Expression.And(whereClause, keyMatch);
        }

        var lambdaExpression = (Expression<Func<TEntity,bool>>)Expression.Lambda(whereClause, entityParameter);

        // Execute against the in-memory entities, which we get from ChangeTracker (but not filtering the state of the entities).
        var entries = context.ChangeTracker.Entries<TEntity>().Select((EntityEntry e) => (TEntity)e.Entity);
        TEntity entity = entries.AsQueryable().Where(lambdaExpression).First(); // First is what triggers the query execution.

        // If found in memory then we're done.
        if (entity != null) { return entity; }

        // Otherwise execute the query against the database.
        return dbSet.Where(lambdaExpression).First();
    }

Upvotes: 1

Sebastian Busek
Sebastian Busek

Reputation: 1032

Find finally arrives into Entity Framework core.

Upvotes: 8

Ilpo Toni
Ilpo Toni

Reputation: 91

Can't comment because of reputation, but if you use RC2 (or later?) you should use

var context = set.GetService<ICurrentDbContext>().Context;

instead of

var context = set.GetService<DbContext>();

Upvotes: 9

sjb-sjb
sjb-sjb

Reputation: 1197

An edit was proposed to change ".First()" to ".FirstOrDefault()" in the very last line of my earlier post. The edit was voted down, but I agree with it. I would expect the function to return null if the key was not found. I would not want it to throw an exception. In most cases I would want to know if the key existed in the set, and handling an exception is a very slow way of figuring that out.

Upvotes: 0

Shaul Behr
Shaul Behr

Reputation: 38003

I've taken some of the previously provided answers and tweaked them to fix a couple of problems:

  • Implicitly captured closure
  • Key shouldn't be hard coded to "Id"

    public static TEntity Find<TEntity>(this DbSet<TEntity> set, params object[] keyValues) where TEntity : class
    {
        var context = set.GetService<DbContext>();
    
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        var key = entityType.FindPrimaryKey();
    
        var entries = context.ChangeTracker.Entries<TEntity>();
    
        var i = 0;
        foreach (var property in key.Properties)
        {
            var i1 = i;
            entries = entries.Where(e => e.Property(property.Name).CurrentValue == keyValues[i1]);
            i++;
        }
    
        var entry = entries.FirstOrDefault();
        if (entry != null)
        {
            // Return the local object if it exists.
            return entry.Entity;
        }
    
        var parameter = Expression.Parameter(typeof(TEntity), "x");
        var query = set.AsQueryable();
        i = 0;
        foreach (var property in key.Properties)
        {
            var i1 = i;
            query = query.Where((Expression<Func<TEntity, bool>>)
             Expression.Lambda(
                 Expression.Equal(
                     Expression.Property(parameter, property.Name),
                     Expression.Constant(keyValues[i1])),
                 parameter));
            i++;
        }
    
        // Look in the database
        return query.FirstOrDefault();
    }
    

Upvotes: 7

Paul Lawrence
Paul Lawrence

Reputation: 317

Not enough reputation to comment, but there is a bug in @Roger-Santana answer when using it in a console app/seperate assembly:

var i = 0;
foreach (var property in key.Properties)
{
    entries = entries.Where(e => e.Property(property.Name).CurrentValue == keyValues[i]);
    i++;
}
var entry = entries.FirstOrDefault();

The value of 'i' is captured in the foreach so that when entries.FirstOrDefault() is called, keyValues[i] has the value of (at least) keyValues[i++], which in my case crashed with an out of index error. A fix would be to copy the value of 'i' through the loop:

var i = 0;
foreach (var property in key.Properties)
{
   var idx =i;
    entries = entries.Where(e =>  e.Property(property.Name).CurrentValue == keyValues[idx]);
    i++;
}
var entry = entries.FirstOrDefault();

Upvotes: 5

bricelam
bricelam

Reputation: 30375

Here's a very crude, incomplete, and untested implementation of .Find() as an extension method. If nothing else, it should get you pointed in the right direction.

The real implementation is tracked by #797.

static TEntity Find<TEntity>(this DbSet<TEntity> set, params object[] keyValues)
    where TEntity : class
{
    var context = ((IAccessor<IServiceProvider>)set).Service.GetService<DbContext>();

    var entityType = context.Model.GetEntityType(typeof(TEntity));
    var key = entityType.GetPrimaryKey();

    var entries = context.ChangeTracker.Entries<TEntity>();

    var i = 0;
    foreach (var property in key.Properties)
    {
        var keyValue = keyValues[i];
        entries = entries.Where(e => e.Property(property.Name).CurrentValue == keyValue);
        i++;
    }

    var entry = entries.FirstOrDefault();
    if (entry != null)
    {
        // Return the local object if it exists.
        return entry.Entity;
    }

    // TODO: Build the real LINQ Expression
    // set.Where(x => x.Id == keyValues[0]);
    var parameter = Expression.Parameter(typeof(TEntity), "x");
    var query = set.Where((Expression<Func<TEntity, bool>>)
        Expression.Lambda(
            Expression.Equal(
                Expression.Property(parameter, "Id"),
                Expression.Constant(keyValues[0])),
            parameter));

    // Look in the database
    return query.FirstOrDefault();
}

Upvotes: 23

A-Sharabiani
A-Sharabiani

Reputation: 19339

I use linq; instead of Find method you can use:

var record = dbSet.SingleOrDefault(m => m.Id == id)

Upvotes: 4

Bryan - S
Bryan - S

Reputation: 99

So...the above find methods worked great, but if you don't have a column named "Id" in your model, the whole thing is going to fail on the following line. I'm not sure why the OP would have put a hardcoded value into this spot

  Expression.Property(parameter, "Id"),

Here's a revision that will fix it for those that name our Id columns appropriately. :)

var keyCompare = key.Properties[0].Name;

        // TODO: Build the real LINQ Expression
        // set.Where(x => x.Id == keyValues[0]);
        var parameter = Expression.Parameter(typeof(TEntity), "x");
        var query = set.Where((Expression<Func<TEntity, bool>>)
            Expression.Lambda(
                Expression.Equal(
                    Expression.Property(parameter, keyCompare),
                    //Expression.Property(parameter, "Id"),
                    Expression.Constant(keyValues[0])),
                parameter));

        // Look in the database
        return query.FirstOrDefault();
    }

This STILL very well could fail if you have more than one Key setup on the entity object and the key you're looking up by isn't the first, but it should be quite a bit btter this way.

Upvotes: 5

Roger Santana
Roger Santana

Reputation: 191

In case you are using EF 7.0.0-rc1-final, below you find a small update for the code presented by @bricelam in the previous answer. By the way, thank you very much @bricelam - your code was extremely useful for me.

Here are my dependencies under "project.config":

"dependencies": {
    "EntityFramework.Commands": "7.0.0-rc1-final",
    "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
    "Microsoft.Framework.Configuration.Json": "1.0.0-beta8",
    "Microsoft.Framework.ConfigurationModel": "1.0.0-beta4",
    "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta4",
    "Microsoft.Framework.DependencyInjection": "1.0.0-beta8"
}

And below is the extension method for DbSet.Find(TEntity):

using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Infrastructure;
using System;
using System.Linq;
using System.Linq.Expressions;

namespace Microsoft.Data.Entity.Extensions
{
    public static class Extensions
    {
        public static TEntity Find<TEntity>(this DbSet<TEntity> set, params object[] keyValues) where TEntity : class
        {
            var context = ((IInfrastructure<IServiceProvider>)set).GetService<DbContext>();

            var entityType = context.Model.FindEntityType(typeof(TEntity));
            var key = entityType.FindPrimaryKey();

            var entries = context.ChangeTracker.Entries<TEntity>();

            var i = 0;
            foreach (var property in key.Properties)
            {
                entries = entries.Where(e => e.Property(property.Name).CurrentValue == keyValues[i]);
                i++;
            }

            var entry = entries.FirstOrDefault();
            if (entry != null)
            {
                // Return the local object if it exists.
                return entry.Entity;
            }

            // TODO: Build the real LINQ Expression
            // set.Where(x => x.Id == keyValues[0]);
            var parameter = Expression.Parameter(typeof(TEntity), "x");
            var query = set.Where((Expression<Func<TEntity, bool>>)
                Expression.Lambda(
                    Expression.Equal(
                        Expression.Property(parameter, "Id"),
                        Expression.Constant(keyValues[0])),
                    parameter));

            // Look in the database
            return query.FirstOrDefault();
        }
    }
}

Upvotes: 19

ditsikts
ditsikts

Reputation: 296

here is what I use. Not a find method, but works like a charm

var professionalf = from m in _context.Professionals select m;
professionalf = professionalf.Where(s => s.ProfessionalId == id);
Professional professional = professionalf.First();

Upvotes: 0

Related Questions