Convert anonymous type to new C# 7 tuple type
Asked Answered
S

5

39

The new version of C# is there, with the useful new feature 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);
}

Is there a way to convert my anonymous type object obj to the tuple that I want to return without mapping property by property (assuming that the names of the properties match)?

The context is in a ORM, my SomeType object has a lot of other properties and it is mapped to a table with lot of columns. I wanna do a query that brings just ID and NAME, so I need to convert the anonymous type into a tuple, or I need that an ORM Linq Provider know how to understand a tuple and put the properties related columns in the SQL select clause.

Subrogation answered 13/4, 2017 at 19:16 Comment(2)
I think you can just do return (obj.id, obj.name); since you have the names in the function signature, but I don't have C# 7 right now to test it.Suffumigate
Wait, are you sure your anonymous class is new { id => o.Id, name => o.Name } and not new { id = o.Id, name = o.Name }Suffumigate
C
26

The short answer is no, in the current form of C#7 there is no in-framework way to accomplish your goals verbatim, since you want to accomplish:

  • Linq-to-entities
  • Mapping to a subset of columns
  • Avoiding property by property mapping from a custom or anonymous type to a C#7 tuple by mapping directly to a C#7 tuple.

Because Query<SomeType> exposes an IQueryable, any sort of projection must be made to an expression tree .Select(x => new {}).

There is an open roslyn issue for adding this support, but it doesn't exist yet.

As a result, until this support is added, you can either manually map from an anonymous type to a tuple, or return the entire record and map the result to a tuple directly to avoid two mappings, but this is obviously inefficient.


While this restriction is currently baked into Linq-to-Entities due to a lack of support and the inability to use parametered constructors in a .Select() projection, both Linq-to-NHibernate and Linq-to-Sql allow for a hack in the form of creating a new System.Tuple in the .Select() projection, and then returning a ValueTuple with the .ToValueTuple() extension method:

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

Since System.Tuple can be mapped to an expression, you can return a subset of data from your table and allow the framework to handle mapping to your C#7 tuple. You can then deconstruct the arguments with any naming convention you choose:

(int id, string customName) info = GetSomeInfo();
Console.Write(info.customName);
Champerty answered 16/4, 2017 at 5:23 Comment(1)
Or you can use System.ValueTuple instead of System.Tuple if you have named your tuples.Sunroom
B
27

Of course, by creating the tuple from your LINQ expression:

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

    return obj;
}

According to another answer regarding pre-C# 7 tuples, you can use AsEnumerable() to prevent EF to mix things up. (I have not much experience with EF, but this should do:)

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

    return obj;
}
Banksia answered 13/4, 2017 at 19:20 Comment(27)
Did you actually run this? I suspect you should see CS8143 An expression tree may not contain a tuple literal.Champerty
I did using the latest online build, it does compile.Banksia
I doubt EF supports Tuples, Tuples aren't reference types and members aren't properties. It may compile, but it may not run.Holder
Who said EF? @AkashKava I didn't read it in the question, nor in the tags.Banksia
@PatrickHofman I think the issue is that it hasn't been clarified either way. The OP needs to specify if this is linq to objects or linq to sql since the Query<SomeType> is rather misleading.Champerty
The question is about converting type use of an anonymous type to tuples. @DavidLBanksia
Tuples aren't reference types?Lawerencelawes
No, these are not @Clay. These are structs.Banksia
@Lawerencelawes not C#7 tuples.Champerty
@PatrickHofman and I completely understand what you are saying and I did not downvote your answer. However, that said, there's a 50/50 chance that this answer simply will not work if the OP failed to understand the implications of their tagging choices. At the very least you should call the distinction out in your answer.Champerty
@DavidL I rarely use EF, so I don't want to suggest any solution in that field which I need to check. Since others seem to downvote for that, I have to unfortunately.Banksia
@Ðаn Although I like the question you pose, I think it is totally different from the question here. I don't read OP wants to use reflection or nifty stuff to create the tuple. OP just doesn't know how to construct one from an expression.Banksia
@PatrickHofman I can't get this to compile with the latest Linqpad build (5.22.03) which supports C#7 tuples natively without a NuGet package. What online compiler tool are you using? I still suspect a .Select() cannot deconstruct a ValueTuple and the online tool is mis-compiling.Champerty
@DavidL (id: obj.Id), etc. instead of Id, or not creating the anonymous type at all.Banksia
@DavidL the source looks okayBanksia
@PatrickHofman manually "spreading" the tuple properties with strong naming isn't working, nor is name inference.Champerty
@PatrickHofman none of those linked examples are compatible with what you're doing in a .Select(). It isn't a compatible example.Champerty
@DavidL: It really depends on the type returned by Query<SomeType>() and we don't know that yet. If it's IEnumerable<SomeType> then this will compile because no expression trees are required. If it's IQueryable<SomeType> then yes, it will fail to compile.Grassgreen
@JonSkeet oh, right, of course...I was scratching my head on that for an embarrassingly long time. Thank you :)Champerty
@JonSkeet Won't the second one compile, as was suggested in the referenced post from Marc?Banksia
@PatrickHofman yes, the second will, but it will also materialize the entire dataset into memory if it is linq-to-sql, which would most likely not be what the OP wants, since they only want one record, regardless of the source.Champerty
I thought EF was streaming, so it would just send the first batch over. Indeed, less efficient. It might help if you would pull the First() in front of the AsEnumerable(). @DavidLBanksia
@PatrickHofman no, there are actually three different types of EF operations. In this case, .First() is an immediate execution operator, not a streaming operator. And yes, you could use .First() before .AsEnumerable(), but that would defeat the point of projecting a tuple in .Select and you might as well manually spread the object that point. That's why distinguishing between linq to objects and linq to sql is so important...it can dramatically change the effectiveness of the answer and in this case the OP left us hanging :(.Champerty
@DavidL Sorry, I posted the question before I left my work... I've already edited the question. Thanks.Subrogation
@Jaider in the context of an Entity Framework query that is projecting sql?Champerty
@DavidL Absolutely! you can verify it with SQL Server Profiler.Myrlemyrlene
@Myrlemyrlene specifically what version of the library? That is probably a newer feature.Champerty
C
26

The short answer is no, in the current form of C#7 there is no in-framework way to accomplish your goals verbatim, since you want to accomplish:

  • Linq-to-entities
  • Mapping to a subset of columns
  • Avoiding property by property mapping from a custom or anonymous type to a C#7 tuple by mapping directly to a C#7 tuple.

Because Query<SomeType> exposes an IQueryable, any sort of projection must be made to an expression tree .Select(x => new {}).

There is an open roslyn issue for adding this support, but it doesn't exist yet.

As a result, until this support is added, you can either manually map from an anonymous type to a tuple, or return the entire record and map the result to a tuple directly to avoid two mappings, but this is obviously inefficient.


While this restriction is currently baked into Linq-to-Entities due to a lack of support and the inability to use parametered constructors in a .Select() projection, both Linq-to-NHibernate and Linq-to-Sql allow for a hack in the form of creating a new System.Tuple in the .Select() projection, and then returning a ValueTuple with the .ToValueTuple() extension method:

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

Since System.Tuple can be mapped to an expression, you can return a subset of data from your table and allow the framework to handle mapping to your C#7 tuple. You can then deconstruct the arguments with any naming convention you choose:

(int id, string customName) info = GetSomeInfo();
Console.Write(info.customName);
Champerty answered 16/4, 2017 at 5:23 Comment(1)
Or you can use System.ValueTuple instead of System.Tuple if you have named your tuples.Sunroom
H
14

While tuple literals are not currently supported in expression trees, it doesn't mean the ValueTuple type isn't. Just create it explicitly.

public (int id, string name) GetSomeInfo() =>
    Query<SomeType>()
        .Select(o => ValueTuple.Create(o.Id, o.Name))
        .First();
Hotien answered 8/11, 2018 at 21:31 Comment(1)
..which does compile, but fail at runtime if the implementation of the IQueryable does not know about ValueTuple, such as EF6Tragedienne
I
3

Note for anyone on a lower version of .NET: If you are on a lower version of .NET than 4.7.2 or .NET Core, you should use Nuget Package Manager to install System.ValueTuple to your project.

Then, here's an example of getting a tuple from a Linq to SQL query:

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()

That ran for me, however, after check-in, I got a build failure...read on.

For even more fun and games, I unfortunately had an earlier version of C# on my build server for some reason. So I had to go back because it didn't recognize the new tuple format on the .select(o => (o.record1, o.record2)) line (specifically that it would be a tuple because of the parenthesis around o.record1 and o.record2). So, I had to go back and kind of finagle it a bit more:

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;
Implausibility answered 15/5, 2019 at 21:32 Comment(1)
Thank you! Lot's of answers wouldn't (seem) to work for me because I wanted to be able to join tables. This works perfect for me!Prophylactic
V
-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();
    }

This method you can named your tuple item in a linq select

Vc answered 17/4, 2019 at 9:8 Comment(1)
With this code, you lost the benefits of IQueryableCahoon

© 2022 - 2024 — McMap. All rights reserved.