将匿名类型转换为新的C# 7元组类型

38

新版本的C#发布了,其中一个实用的新功能是元组类型(Tuple Types):

public IQueryable<T> Query<T>();

public (int id, string name) GetSomeInfo() {
    var obj = Query<SomeType>()
        .Select(o => new {
            id = o.Id,
            name = o.Name,
        })
        .First();

    return (id: obj.id, name: obj.name);
}

有没有一种方法可以将我的匿名类型对象obj转换为我想要返回的元组,而不必逐个映射属性(假设属性名称匹配)?

背景是在ORM中,我的SomeType对象有许多其他属性,并且映射到具有许多列的表。我想执行一个查询,只带回ID和NAME,因此我需要将匿名类型转换为元组,或者我需要让ORM Linq Provider知道如何理解元组,并将与属性相关的列放入SQL select子句中。


我认为你可以只需执行 return (obj.id, obj.name);,因为你在函数签名中已经有了这些名称,但是我现在没有 C# 7 来测试它。 - juharr
4
等一下,你确定你的匿名类是 new { id => o.Id, name => o.Name } 而不是 new { id = o.Id, name = o.Name } 吗? - juharr
5个回答

26

当然,通过从您的LINQ表达式创建元组:

public (int id, string name) GetSomeInfo() {
    var obj = Query<SomeType>()
        .Select(o => (o.Id,o.Name))
        .First();

    return obj;
}
根据关于C# 7之前元组的另一个答案,您可以使用AsEnumerable()来防止EF混淆。 (我对EF的经验不多,但这应该管用:))
public (int id, string name) GetSomeInfo() {
    var obj = Query<SomeType>()
        .AsEnumerable()
        .Select(o => (o.Id,o.Name))
        .First();

    return obj;
}

20
你实际运行过这个代码吗?我怀疑你会看到 CS8143 An expression tree may not contain a tuple literal. 的错误提示。 - David L
4
谁说了EF?@AkashKava,我在问题或标签中都没有看到这个信息。 - Patrick Hofman
6
问题在于还没有明确。发帖人需要明确是针对对象的 LINQ 还是针对 SQL 的 LINQ,因为“Query<SomeType>”这个名称会有些误导性。 - David L
5
@DavidL:这实际上取决于Query<SomeType>()返回的类型,我们还不知道。如果它是 IEnumerable<SomeType>,那么这段代码将会编译通过,因为不需要表达式树。如果它是 IQueryable<SomeType>,那么是的,它将无法编译通过。 - Jon Skeet
2
我以为EF是流式传输,所以它只会发送第一批数据。确实,效率较低。如果您将First()放在AsEnumerable()之前可能会有所帮助。@DavidL - Patrick Hofman
显示剩余22条评论

25

简短回答是不行,在当前的 C#7 版本中无法通过框架实现你所要达到的目标,因为你想要实现:

  • Linq to entities
  • 将数据映射到一个子集合的列
  • 通过直接映射到 C#7 元组来避免从自定义或匿名类型到 C#7 元组的逐个属性映射。

因为 Query<SomeType> 暴露了一个 IQueryable,任何形式的投影都必须转换成表达式树 .Select(x => new {})

目前有一个开放的 Roslyn 问题,旨在添加此支持,但目前还不存在。

因此,在此支持被添加之前,你可以手动从匿名类型映射到元组,或者直接将整个记录返回并将结果映射到元组以避免两次映射,但这显然效率低下。


虽然由于缺乏支持并且无法在 .Select() 投影中使用带参数的构造函数,这种限制目前已经固定在 Linq-to-Entities 中,但是 Linq-to-NHibernate 和 Linq-to-Sql 都允许通过在 .Select() 投影中创建新的 System.Tuple 并返回使用 .ToValueTuple() 扩展方法的 ValueTuple 的 hack 来实现。

public IQueryable<T> Query<T>();

public (int id, string name) GetSomeInfo() {
    var obj = Query<SomeType>()
        .Select(o => new System.Tuple<int, string>(o.Id, o.Name))
        .First();

    return obj.ToValueTuple();
}

由于System.Tuple可以映射到表达式,因此您可以从表中返回一部分数据并允许框架处理映射到C#7元组。然后,您可以使用任何命名约定拆分参数:

(int id, string customName) info = GetSomeInfo();
Console.Write(info.customName);

5
如果你已经为你的元组命名,你可以使用System.ValueTuple代替System.Tuple - Babak

14

虽然元组字面量目前不支持表达式树,但这并不意味着ValueTuple类型不支持。只需显式创建即可。

public (int id, string name) GetSomeInfo() =>
    Query<SomeType>()
        .Select(o => ValueTuple.Create(o.Id, o.Name))
        .First();

1
这段代码可以编译通过,但是如果 IQueryable 的实现不知道 ValueTuple(例如 EF6),则在运行时会失败。 - M.Stramm

3
任何使用低版本.NET的人请注意: 如果您使用的.NET版本低于4.7.2或.NET Core,请使用Nuget软件包管理器将System.ValueTuple安装到您的项目中。
然后,这是从Linq to SQL查询获取元组的示例:
var myListOfTuples = (from record1 in myTable.Query()
                     join record2 in myTable2.Query() on record1.Id = record2.someForeignKey
                     select new {record1, record2}).AsEnumerable()
                     .select(o => (o.record1, o.record2))
                     .ToList()

尽管运行正常,但在签入后,我遇到了构建失败的问题...请继续阅读。

更有趣的是,由于某种原因,我的构建服务器上安装的是早期版本的C#。 因此,我不得不回退版本,因为它无法识别.select(o => (o.record1, o.record2))行上的新元组格式(具体而言,它会将o.record1和o.record2用括号括起来表示为元组)。 所以,我不得不返回并进行一些调整:

var myListOfAnonymousObjects = (from record1 in myTable.Query()
                     join record2 in myTable2.Query() on record1.Id = record2.someForeignKey
                     select new {record1, record2}).ToList()

var myTuples = new List<Tuple<Record1sClass, Record2sClass>>();
foreach (var pair in myListOfAnonymousObjects)
{
    myTuples.Add(pair.record1, pair.record2);
}
return myTuples;

谢谢!很多答案似乎对我不起作用,因为我想能够连接表。这对我来说完美无缺! - Caleb W.

-2
    public IList<(int DictionaryId, string CompanyId)> All(string DictionaryType)
    {
        var result = testEntities.dictionary.Where(p => p.Catalog == DictionaryType).ToList();
        var resultTuple = result.Select(p => (DictionaryId: p.ID, CompanyId: p.CompanyId));
        return resultTuple.ToList();
    }

这种方法可以在linq选择中为元组项命名


使用这段代码,您将失去 IQueryable 的好处。 - Orace

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