C# EF Core如何通过字符串表名、字段名动态查询数据库数据
C# EF Core 动态传入表/视图名、字段名实现查询
问题背景
我在C#项目配置中存储了待取数的表/视图名称、对应字段名称,配置片段如下:
new() { DisplayIndex = 1082, FieldNameDomain = nameof(VwScoringJobSupplier.SchemeIdentifications), Label = "Identifications", DataType = "string", Group = "Attributes", IsAnalysisFilter = false, IsUseCaseFilter = false, FilterType = "multiselect", IsAvailableForScoringAdmin = false, Source = "VwScoringJobSuppliers" },
配置字段说明:
FieldNameDomain:存储查询目标字段名Source:存储查询目标表/视图名
最初我写了固定查询VwScoringJobSuppliers视图指定字段的GetOptions方法,代码如下:
private async Task<List<OptionDto>> GetOptions(Guid jobId, string source, string field) { var options = await _dbContext.VwScoringJobSuppliers.Distinct().Where(x => x.JobId == jobId) .Select(x => x.State).ToListAsync(); }
当前需求:查询使用的表/视图、Select选取的字段必须从字符串参数source、field动态传入。
已完成进度
目前已经实现了反射获取对应DbSet属性的工具方法,代码如下:
public static PropertyInfo GetDbSetProperty(Type typeOfEntity, ScoringDbContext context) { var genericDbSetType = typeof(DbSet<>); var entityDbSetType = genericDbSetType.MakeGenericType(typeOfEntity); var contextType = context.GetType(); return contextType .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType == entityDbSetType) .FirstOrDefault(); }
当前卡点:不知道如何将获取到的DbSet结合到动态查询逻辑中,最初尝试的FromSqlRaw写法如下,但无法正常运行:
var blogs = context.Blogs .FromSqlRaw("SELECT {0} FROM dbo.{1}", source, field) .ToList();
具体实现方案
核心注意点
不要直接通过FromSqlRaw的参数化语法传表名、字段名:SQL参数仅支持传递值类型数据,传递数据库对象名(表、列)会被转义为字符串常量,导致SQL语法错误。表名、字段名必须做白名单校验后拼接,避免SQL注入风险。
方案1:EF Core 7.0+ 最简实现(推荐)
EF Core 7.0及以上版本支持Database.SqlQueryRaw<TResult>直接执行SQL返回标量结果,不需要绑定完整实体的DbSet,适配当前只查询单个字段去重值的场景:
- 先配置数据源白名单,启动时初始化,所有允许查询的表/视图都加入映射:
// 配置白名单,从根源避免SQL注入 private static readonly IReadOnlyDictionary<string, Type> SourceEntityMap = new Dictionary<string, Type> { ["VwScoringJobSuppliers"] = typeof(VwScoringJobSupplier), // 后续新增的查询源在这里补充映射即可 };
- 实现动态查询逻辑:
private async Task<List<OptionDto>> GetOptions(Guid jobId, string source, string field) { // 校验表名合法性 if (!SourceEntityMap.ContainsKey(source)) throw new ArgumentException($"不支持的数据源: {source}"); // 校验字段合法性 var entityType = SourceEntityMap[source]; var fieldExist = entityType.GetProperty(field, BindingFlags.Public | BindingFlags.Instance) != null; if (!fieldExist) throw new ArgumentException($"数据源{source}不存在字段: {field}"); // 白名单校验通过后拼接SQL,JobId通过参数化传入,无注入风险 var sql = $"SELECT DISTINCT [{field}] FROM dbo.[{source}] WHERE JobId = @JobId"; var fieldValues = await _dbContext.Database .SqlQueryRaw<string>(sql, new SqlParameter("@JobId", jobId)) .ToListAsync(); // 转换为OptionDto返回,根据你自己的DTO结构调整映射逻辑 return fieldValues.Select(value => new OptionDto { Value = value, Label = value }).ToList(); }
方案2:兼容EF Core 6及以下版本(反射+表达式树)
如果使用低版本EF Core,可以复用你已经写好的GetDbSetProperty方法,通过动态构造表达式树实现强类型查询,不需要手写SQL:
private async Task<List<OptionDto>> GetOptions(Guid jobId, string source, string field) { // 白名单校验逻辑和方案1一致 if (!SourceEntityMap.TryGetValue(source, out var entityType)) throw new ArgumentException($"不支持的数据源: {source}"); var fieldProp = entityType.GetProperty(field, BindingFlags.Public | BindingFlags.Instance); if (fieldProp == null) throw new ArgumentException($"数据源{source}不存在字段: {field}"); // 1. 反射获取对应DbSet var dbSetProp = GetDbSetProperty(entityType, _dbContext); var dbSet = dbSetProp.GetValue(_dbContext) as IQueryable; // 2. 构造 Where(x => x.JobId == jobId) 条件 var param = Expression.Parameter(entityType, "x"); var jobIdProp = entityType.GetProperty("JobId") ?? throw new InvalidOperationException($"数据源{source}缺少JobId字段"); var whereLambda = Expression.Lambda( Expression.Equal( Expression.Property(param, jobIdProp), Expression.Constant(jobId) ), param); var whereQuery = dbSet.Provider.CreateQuery( Expression.Call(typeof(Queryable), "Where", new[] { entityType }, dbSet.Expression, whereLambda) ); // 3. 追加 Distinct() var distinctQuery = dbSet.Provider.CreateQuery( Expression.Call(typeof(Queryable), "Distinct", new[] { entityType }, whereQuery.Expression) ); // 4. 构造 Select(x => x.目标字段) 投影 var selectLambda = Expression.Lambda(Expression.Property(param, fieldProp), param); var selectQuery = distinctQuery.Provider.CreateQuery<string>( Expression.Call(typeof(Queryable), "Select", new[] { entityType, typeof(string) }, distinctQuery.Expression, selectLambda) ); // 5. 执行查询并映射结果 var fieldValues = await selectQuery.ToListAsync(); return fieldValues.Select(value => new OptionDto { Value = value, Label = value }).ToList(); }
补充说明
- 如果配置中
DataType不是string类型,可以根据配置值动态调整SqlQueryRaw的泛型参数,或者在表达式树版本中增加类型转换逻辑。 - 白名单校验不要省略,哪怕表名、字段名来自固定配置,也可以避免配置被恶意篡改后引发SQL注入问题。
内容的提问来源于stack exchange,提问作者Eugene Sukh
相关产品推荐
相关产品推荐

