web-dev-qa-db-fra.com

passer le type d'entité en paramètre dans linq

Comment procéder pour passer un type d'entité en tant que paramètre dans linq?

Par exemple La méthode recevra la valeur du nom d’entité sous forme de chaîne et je voudrais passer le nom de l’entité à la requête linq ci-dessous. Est-il possible de rendre la requête linq générique?

public ActionResult EntityRecords(string entityTypeName)
{
    var entityResults = context.<EntityType>.Tolist();
    return View(entityResults);
}

Je voudrais passer le type d'entité en tant que paramètre et renvoyer toutes les valeurs de propriété.

En outre, est-il possible de filtrer les résultats en fonction de la propriété some?

9
user793468

En supposant que votre classe context ressemble à ceci:

public class MyContext : DbContext
{
    public DbSet<Entity1> Entity1 { get; set; }
    public DbSet<Entity2> Entity2 { get; set; }

    // and so on ...
}

la solution la plus simple consiste à écrire une méthode qui ressemble à

private List<object> Selector(string entityTypeName)
{
  if (entityTypeName == "Entity1") 
    return context.Entity1.ToList();

  if (entityTypeName == "Entity2")
    return context.Entity2.ToList()

  // and so on  

  // you may add a custom message here, like "Unknown type"
  throw new Exception(); 
}

Mais nous ne voulons pas coder en dur ce genre de choses, alors allons créer Selector de manière dynamique avec Linq.Expressions

Définissez un champ Func dans votre contrôleur:

private readonly Func<string, List<object>> selector;

Maintenant, vous pouvez créer une usine pour ce membre:

private Func<string, List<object>> SelectByType()
{
    var myContext = Expression.Constant(context);
    var entityTypeName = Expression.Parameter(typeof(string), "entityTypeName");

    var label = Expression.Label(typeof(List<object>));
    var body = Expression.Block(typeof(MyContext).GetProperties()
        .Where(p => typeof(IQueryable).IsAssignableFrom(p.PropertyType) && p.PropertyType.IsGenericType)
        .ToDictionary(
            k => Expression.Constant(k.PropertyType.GetGenericArguments().First().Name),
            v => Expression.Call(typeof(Enumerable), "ToList", new[] {typeof(object)}, Expression.Property(myContext, v.Name))
        )
        .Select(kv =>
            Expression.IfThen(Expression.Equal(kv.Key, entityTypeName),
              Expression.Return(label, kv.Value))
        )
        .Concat(new Expression[]
        {
            Expression.Throw(Expression.New(typeof(Exception))),
            Expression.Label(label, Expression.Constant(null, typeof(List<object>))),
        })
    );

    var lambda = Expression.Lambda<Func<string, List<object>>>(body, entityTypeName);
    return lambda.Compile();
}

et assigner Func avec elle (quelque part dans le constructeur)

selector = SelectByType();

Maintenant, vous pouvez l'utiliser comme

public ActionResult EntityRecords(string entityTypeName)
{
    var entityResults = selector(entityTypeName);
    return View(entityResults);
}
4
Aleks Andreev

Vous avez deux options:

Option 1: Vous connaissez le type d'entité au moment de la compilation

Si vous connaissez le type d'entité au moment de la compilation, utilisez une méthode générique:

public ActionResult EntityRecords<TEntity>()
{
    var entityResults = context.Set<TEntity>.ToList();
    return View(entityResults);
}

Usage:

public ActionResult UserRecords()
{
    return EntityRecords<User>();
}

Option 2: vous ne connaissez le type d'entité qu'au moment de l'exécution

Si vous voulez réellement transmettre le type d'entité en tant que chaîne, utilisez l'autre surcharge de Set qui prend un type:

public ActionResult EntityRecords(string entityType)
{
    var type = Type.GetType(entityType);
    var entityResults = context.Set(type).ToList();
    return View(entityResults);
}

Cela suppose que entityType est un nom de type complet, y compris Assembly. Voir cette réponse pour plus de détails.
Si les entités sont toutes dans le même assemblage que le contexte - ou dans un autre assemblage bien connu - vous pouvez utiliser ce code à la place pour obtenir le type d'entité:

var type = context.GetType().Assembly.GetType(entityType);

Cela vous permet d'omettre l'assembly dans la chaîne, mais requiert toujours l'espace de nom.

3
Daniel Hilgarth

Vous pouvez obtenir ce que vous voulez même si le contexte n'a pas de propriété DbSet (et si c'est le cas, cela ne nuit pas). C'est en appelant la méthode DbContext.Set<TEntity>() par réflexion:

var nameSpace = "<the full namespace of your entity types here>";

// Get the entity type:
var entType = context.GetType().Assembly.GetType($"{nameSpace}.{entityTypeName}");

// Get the MethodInfo of DbContext.Set<TEntity>():
var setMethod = context.GetType().GetMethods().First(m => m.Name == "Set" && m.IsGenericMethod);
// Now we have DbContext.Set<>(), turn it into DbContext.Set<TEntity>()
var genset = setMethod.MakeGenericMethod(entType);

// Create the DbSet:
var dbSet = genset.Invoke(context, null);

// Call the generic static method Enumerable.ToList<TEntity>() on it:
var listMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(entType);
var entityList = listMethod.Invoke(null, new[] { dbSet });

Vous avez maintenant votre liste d'entités.

Une remarque: pour réduire l'impact sur les performances dû aux réflexions, vous pouvez mettre en cache des types et des informations de méthodes non génériques.

Autre remarque: je ne pense pas que je recommanderais ceci. Comme indiqué dans un commentaire: cela soulève quelques préoccupations. Par exemple: autoriserez-vous une application cliente à obtenir toutes les données non filtrées de la table d'entités any? Quoi que vous fassiez: manipulez avec précaution.

1
Gert Arnold

Dans votre exemple, il semble qu'une action de contrôleur utilise le nom de l'entité en tant que paramètre. Par conséquent, vous ne pourrez pas rendre votre méthode générique. Mais vous pouvez utiliser la réflexion et éviter l'utilisation de génériques pour la plupart.

public ActionResult EntityRecords(string entityTypeName)
{
    var entityProperty = context.GetType().GetProperty(entityTypeName);
    var entityQueryObject = (IQueryable)entityProperty.GetValue(context);
    var entityResults = entityQueryObject.Cast<object>().ToList();
    return View(entityResults);
}

Il y a cependant quelques points à garder à l'esprit:

  1. L'hypothèse est que vous avez une propriété sur votre contexte correspondant à l'argument entityTypeName donné. Si entityTypeName est en fait le nom du type au lieu du nom de la propriété, vous devrez effectuer un travail supplémentaire pour trouver la propriété appropriée.
  2. Votre vue devra savoir quoi faire avec une collection d’objets dont le type n’est pas connu au moment de la compilation. Il faudra probablement utiliser la réflexion pour faire ce que vous avez l'intention de faire.
  3. Il peut y avoir des problèmes de sécurité dans une méthode comme celle-ci. Par exemple, si l'utilisateur fournit "Base de données" ou "Configuration", vous pourriez éventuellement exposer des informations telles que votre chaîne de connexion, ce qui n'a rien à voir avec les entités que vous avez stockées.

En outre, est-il possible de filtrer les résultats en fonction de la propriété some?

Oui, et cela impliquera un usage similaire de réflexion et/ou dynamic. Vous pouvez utiliser une bibliothèque du type Dynamic LINQ pour passer des chaînes dans les surcharges de méthodes analogues à LINQ (Where, Select, etc.). 

public ActionResult EntityRecords(string entityTypeName, FilterOptions options)
{
    var entityProperty = context.GetType().GetProperty(entityTypeName);
    var entityQueryObject = entityProperty.GetValue(context);
    var entityResults = ApplyFiltersAndSuch((IQueryable)entityQueryObject);
    return View(entityResults);
}

private IEnumerable<object> ApplyFiltersAndSuch(IQueryable query, FilterOptions options)
{
    var dynamicFilterString = BuildDynamicFilterString(options);
    return query.Where(dynamicFilterString)
        // you can add .OrderBy... etc.
        .Cast<object>()
        .ToList();
}
0
StriplingWarrior