在 linq 中将实体类型作为参数传递
passing entity type as parameter in linq
我将如何在 linq 中将实体类型作为参数传递?
例如该方法将接收实体名称值作为字符串,我想将实体名称传递给下面的 linq 查询。是否可以使 linq 查询通用?
public ActionResult EntityRecords(string entityTypeName)
{
var entityResults = context.<EntityType>.Tolist();
return View(entityResults);
}
我想将实体类型作为参数传递,并且 return 所有 属性 值。
此外,是否可以根据某些 属性 筛选结果?
在您的示例中,您似乎有一个将实体名称作为参数的控制器操作,因此您将无法使您的方法通用。但是您可以使用反射并在大多数情况下避免使用泛型。
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);
}
不过,有几点需要牢记:
- 假设您的上下文中有一个 属性 对应于给定的
entityTypeName
参数。如果 entityTypeName
实际上是类型名称而不是 属性 名称,您需要做额外的工作才能找到合适的 属性.
- 您的视图必须知道如何处理在编译时不知道对象类型的对象集合。它可能必须使用反射来执行您希望它执行的任何操作。
- 像这样的方法可能存在一些安全问题。例如,如果用户提供 "Database" 或 "Configuration",您最终可能会暴露连接字符串等信息,这与您存储的实际实体无关。
Also, is it possible to filter results based the some property?
是的,它将涉及类似的反射使用 and/or dynamic
。您可以使用 Dynamic LINQ 之类的库将字符串传递到类似 LINQ 的方法重载(Where
、Select
等)。
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();
}
假设您的 context
class 看起来像这样:
public class MyContext : DbContext
{
public DbSet<Entity1> Entity1 { get; set; }
public DbSet<Entity2> Entity2 { get; set; }
// and so on ...
}
最简单的解决方案是编写如下所示的方法
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();
}
但我们不想硬编码这些东西,所以让我们用 Linq.Expressions
动态创建 Selector
在您的控制器中定义一个 Func
字段:
private readonly Func<string, List<object>> selector;
现在您可以为该成员创建一个工厂:
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();
}
并用它分配 Func
(在构造函数中的某处)
selector = SelectByType();
现在你可以像这样使用它了
public ActionResult EntityRecords(string entityTypeName)
{
var entityResults = selector(entityTypeName);
return View(entityResults);
}
您有两个选择:
选项1:编译时知道实体类型
如果您在编译时知道实体类型,请使用泛型方法:
public ActionResult EntityRecords<TEntity>()
{
var entityResults = context.Set<TEntity>.ToList();
return View(entityResults);
}
用法:
public ActionResult UserRecords()
{
return EntityRecords<User>();
}
选项 2:您仅在运行时知道实体类型
如果您真的想将实体类型作为字符串传递,请使用 Set
的另一个重载,它接受一个类型:
public ActionResult EntityRecords(string entityType)
{
var type = Type.GetType(entityType);
var entityResults = context.Set(type).ToList();
return View(entityResults);
}
这假定 entityType
是一个完全限定的类型名称,包括程序集。有关详细信息,请参阅 this answer。
如果实体都在与上下文相同的程序集中 - 或者在另一个众所周知的程序集中 - 您可以改用此代码来获取实体类型:
var type = context.GetType().Assembly.GetType(entityType);
这允许您省略字符串中的程序集,但它仍然需要命名空间。
即使上下文没有 DbSet
属性,您也可以实现您想要的(即使有,也无妨)。就是通过反射调用DbContext.Set<TEntity>()
方法:
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 });
现在您已获得实体列表。
备注:要消除反射引起的一些性能影响,您可以缓存一些类型和非泛型方法信息。
另一个评论:我不认为我会推荐这个。正如评论中所说:这引起了一些担忧。例如:您是否要允许客户端应用程序获取 any 实体 table 的所有 unfiltered 数据?无论你在做什么:小心处理。
我将如何在 linq 中将实体类型作为参数传递?
例如该方法将接收实体名称值作为字符串,我想将实体名称传递给下面的 linq 查询。是否可以使 linq 查询通用?
public ActionResult EntityRecords(string entityTypeName)
{
var entityResults = context.<EntityType>.Tolist();
return View(entityResults);
}
我想将实体类型作为参数传递,并且 return 所有 属性 值。
此外,是否可以根据某些 属性 筛选结果?
在您的示例中,您似乎有一个将实体名称作为参数的控制器操作,因此您将无法使您的方法通用。但是您可以使用反射并在大多数情况下避免使用泛型。
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);
}
不过,有几点需要牢记:
- 假设您的上下文中有一个 属性 对应于给定的
entityTypeName
参数。如果entityTypeName
实际上是类型名称而不是 属性 名称,您需要做额外的工作才能找到合适的 属性. - 您的视图必须知道如何处理在编译时不知道对象类型的对象集合。它可能必须使用反射来执行您希望它执行的任何操作。
- 像这样的方法可能存在一些安全问题。例如,如果用户提供 "Database" 或 "Configuration",您最终可能会暴露连接字符串等信息,这与您存储的实际实体无关。
Also, is it possible to filter results based the some property?
是的,它将涉及类似的反射使用 and/or dynamic
。您可以使用 Dynamic LINQ 之类的库将字符串传递到类似 LINQ 的方法重载(Where
、Select
等)。
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();
}
假设您的 context
class 看起来像这样:
public class MyContext : DbContext
{
public DbSet<Entity1> Entity1 { get; set; }
public DbSet<Entity2> Entity2 { get; set; }
// and so on ...
}
最简单的解决方案是编写如下所示的方法
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();
}
但我们不想硬编码这些东西,所以让我们用 Linq.Expressions
Selector
在您的控制器中定义一个 Func
字段:
private readonly Func<string, List<object>> selector;
现在您可以为该成员创建一个工厂:
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();
}
并用它分配 Func
(在构造函数中的某处)
selector = SelectByType();
现在你可以像这样使用它了
public ActionResult EntityRecords(string entityTypeName)
{
var entityResults = selector(entityTypeName);
return View(entityResults);
}
您有两个选择:
选项1:编译时知道实体类型
如果您在编译时知道实体类型,请使用泛型方法:
public ActionResult EntityRecords<TEntity>()
{
var entityResults = context.Set<TEntity>.ToList();
return View(entityResults);
}
用法:
public ActionResult UserRecords()
{
return EntityRecords<User>();
}
选项 2:您仅在运行时知道实体类型
如果您真的想将实体类型作为字符串传递,请使用 Set
的另一个重载,它接受一个类型:
public ActionResult EntityRecords(string entityType)
{
var type = Type.GetType(entityType);
var entityResults = context.Set(type).ToList();
return View(entityResults);
}
这假定 entityType
是一个完全限定的类型名称,包括程序集。有关详细信息,请参阅 this answer。
如果实体都在与上下文相同的程序集中 - 或者在另一个众所周知的程序集中 - 您可以改用此代码来获取实体类型:
var type = context.GetType().Assembly.GetType(entityType);
这允许您省略字符串中的程序集,但它仍然需要命名空间。
即使上下文没有 DbSet
属性,您也可以实现您想要的(即使有,也无妨)。就是通过反射调用DbContext.Set<TEntity>()
方法:
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 });
现在您已获得实体列表。
备注:要消除反射引起的一些性能影响,您可以缓存一些类型和非泛型方法信息。
另一个评论:我不认为我会推荐这个。正如评论中所说:这引起了一些担忧。例如:您是否要允许客户端应用程序获取 any 实体 table 的所有 unfiltered 数据?无论你在做什么:小心处理。