在运行时使用Entity Framework动态选择列

10

我有一个现有的函数,像这样

public int sFunc(string sCol , int iId)
{
    string sSqlQuery = "  select  " + sCol + " from TableName where ID = " +  iId ;
    // Executes query and returns value in column sCol
}
Int 1 Int 2 Int 3 Int 4

Using the above function, I am reading values from each column of the table which stores integers.

Currently, I am migrating to Entity Framework.

public int sFunc(string sCol , int iId)
{
     return Convert.ToInt32(TableRepository.Entities.Where(x => x.ID == iId).Select(x => sCol ).FirstOrDefault());
}

但是上面的函数返回了一个错误

输入字符串格式不正确

因为它返回了列名本身。

我不知道该如何解决,因为我对 EF 还很陌生。

任何帮助将不胜感激。

谢谢。


1
那么,您想在运行时确定选择哪些列?我理解得对吗? - Tieson T.
@TiesonT。是的,完全正确。 - pravprab
6个回答

6

虽然这个问题对于8年后的OP没有用处,但是由于这个问题有很多点击量,因此我认为为其他人提供正确答案可能会有所帮助。

如果您使用Entity Framework,应该使用Linq projection(Select()),因为这会在数据库端生成正确且有效的查询,而不会拉取整个实体。

使用Linq Select()时,通常需要提供lambda表达式,因此在字符串中包含列/属性名称是主要难点。

最简单的解决方法是使用Dynamic LINQ(EntityFramework.DynamicLinq Nuget软件包)。该软件包提供了原始Linq方法的替代方法,它们将字符串作为参数,并将这些字符串转换为适当的表达式。

例如:

async Task<int> GetIntColumn(int entityId, string intColumnName)
{
    return await TableRepository.Entities
        .Where(x => x.Id == entityId)
        .Select(intColumnName) // Dynamic Linq projection
        .Cast<int>()
        .SingleAsync();
}

我还将此转换为async调用,因为现在所有的数据库调用都应该异步执行。当您调用此方法时,必须await它以获取结果(即:var res = await GetIntColumn(...);)。

泛型变体

也许将其更改为IQueryable的扩展方法,并将列/属性类型更改为泛型类型参数会更有用,这样您可以使用任何列/属性:

(前提是您所有实体都具有指定Id属性的公共接口。)

public static async Task<TColumn> GetColumn<TEntity, TColumn>(this IQueryable<TEntity> queryable, int entityId, string columnName)
    where TEntity : IEntity
{
    return await queryable
        .Where(x => x.Id == entityId)
        .Select(columnName) // Dynamic Linq projection
        .Cast<TColumn>()
        .SingleAsync();
}

这样称为:var result = await TableRepository.Entities.GetColumn<Entity, int>(id, columnName);

泛型变量可以接受列的列表

您可以进一步扩展它以支持动态选择多个列:

public static async Task<dynamic> GetColumns<TEntity>(this IQueryable<TEntity> queryable, int entityId, params string[] columnNames)
    where TEntity : IEntity
{
    return await queryable
        .Where(x => x.Id == entityId)
        .Select($"new({string.Join(", ", columnNames)})")
        .Cast<dynamic>()
        .SingleAsync();
}

这被称为这样:var result = await TableRepository.Entities.GetColumns(id, columnName1, columnName2, ...);

由于返回类型及其成员在编译时未知,因此我们必须在这里返回dynamic。这使得与结果一起工作变得困难,但如果您只想将其序列化并发送回客户端,那么对于该目的来说就可以了。


1
这可能有助于解决您的问题:
public int sFunc(string sCol, int iId)
{
    var _tableRepository = TableRepository.Entities.Where(x => x.ID == iId).Select(e => e).FirstOrDefault();
    if (_tableRepository == null) return 0;

    var _value = _tableRepository.GetType().GetProperties().Where(a => a.Name == sCol).Select(p => p.GetValue(_tableRepository, null)).FirstOrDefault();

    return _value != null ? Convert.ToInt32(_value.ToString()) : 0;
}

这种方法现在适用于动态输入的方法参数sCol更新: 这与当前问题无关,但一般情况下我们如何使用表达式来选择动态列:
var parameter = Expression.Parameter(typeof(EntityTable));
var property = Expression.Property(parameter, "ColumnName");
//Replace string with type of ColumnName and entity table name.
var selector = Expression.Lambda<Func<EntityTable, string>>(property, parameter);

//Before using queryable you can include where clause with it. ToList can be avoided if need to build further query.
var result = queryable.Select(selector).ToList();

非常感谢您的帮助,但是这个代码给我报错了(TableRepository.Entities.Entity没有定义'sCol')。 - pravprab
那完美地解决了问题,让我得到了想要的结果。谢谢。 - pravprab
@ArekBal,你想说什么?是的,它与Entity Framework有关,因为我在集合TableRepository.Entities上提供了Linq,该集合是EntityObject的集合,请解释您的问题或为您的问题提供链接,以便我可以为您找到一些东西。是的,它不是动态Linq,请参考Naveen的答案。 - Ankush Madankar
5
这个查询仍然会请求所有列的数据。 - Ömer Cinbat
6
请注意,如果您正在处理实体框架(Entity Framework),这是一个非常糟糕的答案。它仍将从数据库中检索所有数据,并且需要您有一个固定的实体类型。 - DavidG

0

你需要尝试使用动态LINQ。详细信息请参见这里


8
如果链接因某些原因而失效,提供链接网页的摘要将有助于改进此回答。或者,您可以使用该网页详细介绍的工具添加代码示例,这将使答案更加完善。 - Mage Xy
3
你提供的链接中没有关于主题的解释。 - Ömer Cinbat

0
我们多年来一直面临着与Entity Framework 6相关的相同困扰。在我们的情况下,我们的数据网格有300列,而EF 6生成的SQL查询代码有2000行。我们尝试了下面描述的相同优化方法,但结果性能更差。
EF Core(我们在EF Core 7上进行了测试)带来了巨大的提升。
我们的查询通常是这样构建的:
db.MyDbSet
   .GetOverviewQuery()
   .SelectColumns(selectedColumnNames)
   .ToArrayAsync(...);

在方法 GetOverviewQuery() 中包含一个返回 IQueryable 的大型 LINQ select。TResult 只是一个没有逻辑的 DTO 类。

我们引入了一个方法 SelectColumns(),它接收一个从 DTO 类中选择的属性名称列表。

private static IQueryable<TResult> SelectColumns<TResult>(this IQueryable<TResult> source, HashSet<string> selectedColumnNames)
        {
            var props = GetProperties<TResult>(selectedColumnNames);
            var sourceType = source.ElementType;
            var resultType = typeof(TResult);
            var parameter = Expression.Parameter(sourceType, "e");
            var bindings = props
                .Select(prop => Expression.Bind(prop, Expression.PropertyOrField(parameter, prop.Name)));
            var body = Expression.MemberInit(Expression.New(resultType), bindings);
            var selector = Expression.Lambda(body, parameter);
            return source.Provider.CreateQuery<TResult>(
                Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
                    source.Expression, Expression.Quote(selector)));
        }
        
        private static IEnumerable<PropertyInfo> GetProperties<TItem>(HashSet<string> selectedColumnNames)
        {
            return typeof(TItem)
                // Make sure you can use only valid columns with setter
                .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty)
                .Where(p => p.CanWrite)
                .Where(p => !selectedColumnNames.Contains(p.Name));
        }

最后,它减小了最终的SQL大小,因为EF Core会删除所有不在我们列选择中的列。这也优化了嵌套的选择和连接。例如,如果您只选择了10列,原始查询中的所有列将从2000行SQL减少到30行,只考虑必要的数据。这不仅提升了表达式树的构建,也提升了SQL服务器查询的效率。
希望我们早些时候就能在项目中使用EF Core :D 希望它能有所帮助 :)

-1
你可以这样做:
        var entity = _dbContext.Find(YourEntity, entityKey);
        // Finds column to select or update
        PropertyInfo propertyInfo = entity.GetType().GetProperty("TheColumnVariable");

欢迎来到 Stack Overflow!请注意,您正在回答一个非常古老且已经有答案的问题。这里有一个关于如何回答问题的指南。 - help-info.de

-1

不要将字符串列名作为参数传递,尝试传递一个 lambda 表达式,例如:

sFunc(x => x.FirstColumnName, rowId);
sFunc(x => x.SecondColumnName, rowId);
...

这将最终为您提供列名的智能感知,从而避免在列名拼写错误时出现可能的错误。

更多信息请参见此处:C# 将 Lambda 表达式作为方法参数传递

但是,如果您必须保持相同的方法签名,即支持其他/旧代码,则可以尝试以下操作:

public string sFunc(string sCol , int iId)
{
    return TableRepository.Entities.Where(x => x.ID == iId).Select(x => (string) x.GetType().GetProperty(sCol).GetValue(x)});
}

你可能需要稍微调整一下,因为我没有快速测试的方法。

谢谢你的帮助,目前我无法更改我的函数参数。所以你的第二个选项对我来说是合适的。我已经尝试过了,但出现了一个错误(LINQ to Entities 不认识 'System.Object GetValue' 方法)。 - pravprab
我从 GetValue 中删除了第二个参数。请尝试更新的代码。顺便说一下,您可能需要添加一些错误处理,例如如果找不到给定名称的属性 - 可能是拼写错误的列名。 - Floremin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接