web-dev-qa-db-fra.com

Méthode EF Core Find équivalente pour plusieurs enregistrements?

DbSet d'EF Core a une méthode appelée Find qui:

Recherche une entité avec les valeurs de clé primaire données. Si une entité avec les valeurs de clé primaire données est suivie par le contexte, elle est renvoyée immédiatement sans faire de demande à la base de données. Sinon, une requête est effectuée dans la base de données pour une entité avec les valeurs de clé primaire données et cette entité, si elle est trouvée, est attachée au contexte et renvoyée. Si aucune entité n'est trouvée, null est renvoyé.

J'ai besoin de renvoyer plusieurs éléments en fonction du tableau donné de valeurs de clé primaire, le tout en une seule demande bien sûr. Existe-t-il une méthode pour le faire dans EF Core?

Mise à jour : je sais que je peux utiliser la clause Where dans des scénarios normaux. Mais je crée un utilitaire d'aide qui est générique, et dans celui-ci, je n'ai pas accès aux propriétés fortement typées de mon modèle. Je ne peux donc pas utiliser la clause Where(x => ids.Contains(x.Id)).

Mise à jour 2 : La méthode souhaitable peut avoir une signature simple qui obtient une liste de long valeurs et renvoie une liste de T. public static List<T> FindSet(List<long> ids) qui peut être utilisée comme ceci:

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

Comme mentionné dans les commentaires, l'utilisation de Find de manière naïve (par exemple, en parcourant toutes vos valeurs de clé) finira par exécuter une requête pour chaque valeur, de sorte que ce n'est pas ce que vous voudriez faire. La bonne solution consiste à utiliser une requête Where qui récupère tous les éléments à la fois. Le problème ici est simplement que vous devez le demander dynamiquement pour la clé primaire.

Bien sûr, le contexte de la base de données lui-même sait quelle est la clé primaire pour un type d'entité donné. La façon dont Find fonctionne en interne consiste à utiliser ces informations pour créer une requête dynamique dans laquelle il vérifie l'égalité sur la clé primaire. Donc, pour avoir un FindAll, nous devrons faire de même.

Ce qui suit est une solution rapide pour cela. Cela crée essentiellement une requête dbSet.Where(e => keyValues.Contains(e.<PrimaryKey>)) pour vous.

Notez que, comme je le construis, cela ne fonctionne que pour une seule clé primaire par type d'entité. Si vous essayez de l'utiliser avec des clés composées, il lancera un NotSupportedException. Vous devez absolument pouvez développer cela pour ajouter la prise en charge des clés composées; Je n’ai tout simplement pas fait cela car cela rend tout beaucoup plus complexe (d’autant plus que vous ne pouvez pas utiliser Contains alors).

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();
    }
}

L'utilisation est comme ceci:

// 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 });

J'ai également ajouté une validation de base, donc des choses comme context.FindAllAsync<MyEntity>(1, 2, "foo") échoueront tôt.

5
poke

Si vous souhaitez créer une méthode de recherche générique qui trouve toutes les lignes correspondant à une liste de clés primaires, vous pouvez y parvenir en héritant ces types d'entités d'une classe de base dans laquelle ils partagent le même nom pour la colonne Clé primaire. Pensez-y de cette façon: comment se comporterait cette méthode si votre entité (table de base de données) a une clé composite? Donc, si vous pouvez vous conformer à ce type de conception, l'implémentation suivante montre une logique simple pour y parvenir avec .NET Core. (En fait, vous pouvez également obtenir le même comportement avec 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();
    }
}
1
Saeid

Je cherchais récemment la même chose que vous et j'ai fini par l'implémenter moi-même après avoir fait un peu de recherche et quelques essais et erreurs.

Je sais que la question est ancienne mais je pensais que l'autre pourrait chercher une solution à ce besoin (comme je l'ai fait).

En travaillant dans .Net Core 2, j'ai fini par créer 2 méthodes d'extension pour DBContext qui ressemblent à ceci:

    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>;
    }

La deuxième méthode d'extension dépend d'une méthode d'extension IEnumerable appelée CastToList qui ressemble à ceci:

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);
        }
    }
}

J'ai ajouté des commentaires dans le code pour vous aider à mieux comprendre ce que j'ai fait.

Vous pouvez appeler le "FindAll" comme ceci:

yourDbContext.FindAll(entityType, ids)

De toute évidence, cela pourrait ne pas répondre aux besoins de tout le monde tel quel et pourrait nécessiter quelques ajustements pour obtenir le résultat souhaité, mais cela devrait fournir un point de départ solide.

Dans le code ci-dessus, je suppose que la clé primaire n'est composée que d'une seule propriété. Il serait très certainement possible de modifier le code pour couvrir les clés composites mais cela va au-delà de ce que vous cherchiez.

J'espère que cela aidera ceux qui recherchent une solution.

0
Nicolas