EF Core Find方法等效于多条记录?

时间:2018-06-03 18:18:13

标签: c# entity-framework entity-framework-core

EF Core' DbSet有一个名为Find的方法:

  

查找具有给定主键值的实体。如果是实体   上下文跟踪给定的主键值,然后是它   在不向数据库发出请求的情况下立即返回。   否则,将对具有该实体的实体的数据库进行查询   给定主键值,如果找到该实体,则附加到   上下文并返回。如果未找到任何实体,则返回null。

我需要根据给定的主键值数组返回多个项目,当然所有这些都在一个请求中。在EF Core中有没有办法做到这一点?

更新:我知道在正常情况下我可以使用Where子句。但我正在创建一个通用的辅助工具,在其中我无法访问模型的强类型属性。因此,我无法使用Where(x => ids.Contains(x.Id))条款。

更新2 :理想的方法可以使用简单的签名来获取long值列表,并返回T列表。 public static List<T> FindSet(List<long> ids)可以像这样使用:

var foundRecords = dbset.FindSet(new List<long> { 5, 17, 93, 178, 15400 });

4 个答案:

答案 0 :(得分:4)

正如评论中所提到的,以天真的方式使用extern int a;(例如循环遍历所有键值)将最终为每个值运行查询,因此这不是您想要做的。正确的解决方案是使用Find查询一次获取所有项目。这里的问题只是您需要为主键动态请求它。

当然,数据库上下文本身确实知道给定实体类型的主键是什么。 Where内部工作的方式是它使用该信息来构建动态查询,在该查询中检查主键上的相等性。因此,为了获得一些Find,我们必须这样做。

以下是此的快速解决方案。这基本上为您构建了FindAll查询。

请注意,我构建它的方式,它只适用于每个实体类型的单个主键。如果您尝试将它与复合键一起使用,它将抛出dbSet.Where(e => keyValues.Contains(e.<PrimaryKey>))。你绝对可以扩展它,但增加了对复合键的支持;我只是没有这样做,因为它使一切变得更加复杂(特别是因为你不能使用NotSupportedException)。

Contains

用法是这样的:

public static class DbContextFindAllExtensions
{
    private static readonly MethodInfo ContainsMethod = typeof(Enumerable).GetMethods()
        .FirstOrDefault(m => m.Name == "Contains" && m.GetParameters().Length == 2)
        .MakeGenericMethod(typeof(object));

    public static Task<T[]> FindAllAsync<T>(this DbContext dbContext, params object[] keyValues)
        where T : class
    {
        var entityType = dbContext.Model.FindEntityType(typeof(T));
        var primaryKey = entityType.FindPrimaryKey();
        if (primaryKey.Properties.Count != 1)
            throw new NotSupportedException("Only a single primary key is supported");

        var pkProperty = primaryKey.Properties[0];
        var pkPropertyType = pkProperty.ClrType;

        // validate passed key values
        foreach (var keyValue in keyValues)
        {
            if (!pkPropertyType.IsAssignableFrom(keyValue.GetType()))
                throw new ArgumentException($"Key value '{keyValue}' is not of the right type");
        }

        // retrieve member info for primary key
        var pkMemberInfo = typeof(T).GetProperty(pkProperty.Name);
        if (pkMemberInfo == null)
            throw new ArgumentException("Type does not contain the primary key as an accessible property");

        // build lambda expression
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = Expression.Call(null, ContainsMethod,
            Expression.Constant(keyValues),
            Expression.Convert(Expression.MakeMemberAccess(parameter, pkMemberInfo), typeof(object)));
        var predicateExpression = Expression.Lambda<Func<T, bool>>(body, parameter);

        // run query
        return dbContext.Set<T>().Where(predicateExpression).ToArrayAsync();
    }
}

我还添加了一些基本验证,因此// pass in params var result = await dbContext.FindAllAsync<MyEntity>(1, 2, 3, 4); // or an object array var result = await dbContext.FindAllAsync<MyEntity>(new object[] { 1, 2, 3, 4 }); 之类的内容会提前失败。

答案 1 :(得分:1)

如果要创建查找与主键列表匹配的所有行的通用查找方法,可以通过从基类继承这些实体类型来实现此目的,在基类中,它们共享主键列的相同名称。可以这样考虑:如果您的实体(数据库表)具有复合键,该方法将如何表现?因此,如果您可以遵循此类设计,则以下实现显示了使用.NET Core实现此目的的简单逻辑。 (实际上,您也可以使用EF6实现相同的行为)

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

public class MyTable : MyBaseEntity
{
    public string MyProperty { get; set; }
}

public static class RepositoryExtensions
{
    public static IQueryable<T> FindMacthes<T>(this DbContext db, IEnumerable<int> keys)
        where T : MyBaseEntity
        => db.Set<T>().Where(x => keys.Contains(x.Id));

}

class Program
{
    static void Main(string[] args)
    {
        // Initialize your own DbContext.
        var db = new DbContext(null);
        // Usage:
        var lookupKeys = new[] { 1, 2, 3 };
        var results = db.FindMacthes<MyTable>(lookupKeys).ToList();
    }
}

答案 2 :(得分:0)

最近,我一直在寻找与您相同的东西,经过一些研究和反复试验,最终自己实现了这一目标。

我知道这个问题很旧,但我(尽管我也可能会寻求其他解决方案)

在.Net Core 2中工作,最终我为DBContext创建了两个扩展方法,如下所示:

    public static IQueryable Set(this DbContext context, Type T)
    {
        // Get the generic type definition
        MethodInfo method =
            typeof(DbContext).GetMethod(nameof(DbContext.Set), BindingFlags.Public | BindingFlags.Instance);

        // Build a method with the specific type argument you're interested in
        method = method.MakeGenericMethod(T);

        return method.Invoke(context, null) as IQueryable;
    }

    public static IEnumerable<object> FindAll(this DbContext context, Type T, IEnumerable<object> ids)
    {
        // Set the base entity (T) parameter for the lambda and property expressions
        var xParameter = Expression.Parameter(T, "a");

        // Retrieve the primary key name from the model and set the property expression
        var primaryKeyName = context.Model.FindEntityType(T).FindPrimaryKey().Properties.First().Name;
        var xId = Expression.Property(xParameter, primaryKeyName);

        var idType = xId.Type;

        // Set the constant expression with the list of id you want to search for
        var xIds = Expression.Constant(ids, typeof(IEnumerable<object>));

        // Create the Expression call for the CastEnumerable extension method below 
        var xCastEnumerable = Expression.Call(typeof(IEnumerableExtensions), "CastEnumerable",new[]{idType},xIds);

        // Create the expression call for the "Contains" method that will be called on the list
        // of id that was cast just above with the id property expression as the parameter
        var xContainsMethod = Expression.Call(typeof(Enumerable), "Contains",new[]{idType},xCastEnumerable, xId);

        // Create a lambda expression with the "Contains" expression joined with the base entity (T) parameter
        var xWhereLambda = Expression.Lambda(xContainsMethod, xParameter);

        // Get the "Queryable.Where" method info
        var whereMethodInfo = typeof(Queryable).GetMethods().SingleOrDefault(x => x.Name.Equals("Where") && x.GetParameters()[1].ParameterType.GetGenericType().GenericTypeArguments.Length == 2).MakeGenericMethod(T);

        // Call the where method on the DbSet<T> with the lambda expression that compares the list of id with the entity's Id
        return whereMethodInfo.Invoke(null, new object[] {context.Set(T),xWhereLambda}) as IEnumerable<object>;
    }

第二个扩展方法取决于一个名为CastToList的IEnumerable扩展方法,如下所示:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> CastEnumerable<T>(this IEnumerable<object> sourceEnum)
    {
        if(sourceEnum == null)
            return new List<T>();

        try
        {
            // Covert the objects in the list to the target type (T) 
            // (this allows to receive other types and then convert in the desired type)
            var convertedEnum = sourceEnum.Select(x => Convert.ChangeType(x, typeof(T)));
            // Cast the IEnumerable<object> to IEnumerable<T>
            return convertedEnum.Cast<T>();
        }
        catch (Exception e)
        {
            throw new InvalidCastException($"There was a problem converting {sourceEnum.GetType()} to {typeof(IEnumerable<T>)}", e);
        }
    }
}

我在代码中添加了注释,以帮助您更好地了解我的所作所为。

您可以这样称呼“ FindAll”:

yourDbContext.FindAll(entityType, ids)

很显然,这可能无法完全满足每个人的需求,并且可能需要进行一些调整才能达到预期的结果,但它应该提供一个坚实的起点。

在上面的代码中,我假设主键仅由单个属性组成。绝对有可能修改代码以涵盖组合键,但这超出了您的期望。

我希望这会帮助其他人寻找解决方案。

答案 3 :(得分:0)

支持复合键的解决方案

受@poke答案启发的解决方案。它满足以下条件

  • [x]支持复合键
  • [x]一次获取所有项目
  • [x]不可知实体,不需要基础实体,可与任何键和类型一起使用
  • [x] FAST,通过主键查找,大多数SQL引擎默认将其编入索引
  • [x]简单的SQL翻译
  • [x]简单/简单的一种方法摘要(KISS)
  • [x]异步
  • [x] DbContext扩展
        // FIND ALL
        // ===============================================================
        /// <summary>
        /// Tries to get all entities by their primary keys. Return all/partial/empty array of database entities.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="dbContext"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static async Task<TEntity[]> FindAllAsync<TEntity>(this DbContext dbContext, IEnumerable<TEntity> args) where TEntity : class
        {
            return await Task.Run(() => { 
                var dbParameter = Expression.Parameter(typeof(TEntity), typeof(TEntity).Name);

                var properties = dbContext.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey()?.Properties;

                if (properties == null)
                    throw new ArgumentException($"{typeof(TEntity).FullName} does not have a primary key specified.");

                if (args == null)
                    throw new ArgumentNullException($"Entities to find argument cannot be null");

                if (!args.Any())
                    return Enumerable.Empty<TEntity>().ToArray();

                var aggregatedExpression = args.Select(entity =>
                {
                    var entry = dbContext.Entry(entity);

                    return properties.Select(p =>
                    {
                        var dbProp = dbParameter.Type.GetProperty(p.Name); 
                        var left = Expression.Property(dbParameter, dbProp); 

                        var argValue = entry.Property(p.Name).CurrentValue;
                        var right = Expression.Constant(argValue);

                        return Expression.Equal(left, right);
                    })
                    .Aggregate((acc, next) => Expression.And(acc, next));
                })
                .Aggregate((acc, next) => Expression.OrElse(acc, next));

                var whereMethod = typeof(Enumerable).GetMethods().First(m => m.Name == "Where" && m.GetParameters().Length == 2);
                MethodInfo genericWhereMethod = whereMethod.MakeGenericMethod(typeof(TEntity));

                var whereLambda = Expression.Lambda(aggregatedExpression, dbParameter);

                var set = dbContext.Set<TEntity>();
                var func = whereLambda.Compile();

                var result = genericWhereMethod.Invoke(null, new object[] { set, func}) as IEnumerable<TEntity>;

                return result.ToArray();
            });
        }

注释(if)ノ ✲゚ 。⋆