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