C# 7 added Tuples and provides an awesome syntax for accessing them. C# 7.1 improved the usability of tuples further with Tuple Name Inference. However, sometimes you need to access them dynamically and this can be tricky.
Accessing tuples dynamically is tricky because there are only specialized tuple types for tuples with 0 through 7 parameters. For tuples with 8 or more parameters, there is a ValueTuple
type that holds 7 parameters, plus a Rest
field for another ValueTuple
containing any additional values beyond the 7th. For huge tuples, the Rest
field can be used recursively to yield tuples of arbitrary length.
In this post, I detail the ValueTuple types that internally represent tuples and show how to access tuples using reflection. I then show how to access the 8th parameter and beyond and how to use reflection to iterate over all tuple parameters.
ValueTuple Types
There is one ValueTuple
type for each length of tuple up to 7. There is then a special ValueTuple
type that as its 8th parameter, takes another ValueTuple
. Used recursively, tuples of arbitrary length can be created.
Here is a list of the ValueTuple
types:
ValueTuple
ValueTuple<T1>
ValueTuple<T1, T2>
ValueTuple<T1, T2, T3>
ValueTuple<T1, T2, T3, T4>
ValueTuple<T1, T2, T3, T4, T5>
ValueTuple<T1, T2, T3, T4, T5, T6>
ValueTuple<T1, T2, T3, T4, T5, T6, T7>
ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>
Internally, ValueTuples store the tuple parameters in fields named Item1
through Item7
. The final ValueTuple, ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>
, has an extra field named Rest
that stores the next ValueTuple.
The parameter names you assign to tuple fields are merely syntactic sugar provided by C# and the compiler. At runtime these are gone and only the internal tuple names, Item1
through Item7
are available.
For example, in the following code sample, the tuple field first
would be Item1
at runtime and last
would be Item2
.
var name = (first: "John", last: "Smith");
This runtime desugaring, which is known technically as runtime name erasure is why you must use Item1
through Item7
and Rest
to access the tuple values dynamically at runtime. This applies whether you are using dynamic types or reflection.
Accessing Tuple Fields using Reflection
Accessing the first 7 tuple parameters is quite straightforward. Simply use reflection to access the fields with names Item1
through Item7
.
var item = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var value1 = item.GetType().GetField("Item1");
Console.Out.WriteLine(value1.GetValue(item)); // Prints "1"
var value7 = item.GetType().GetField("Item7");
Console.Out.WriteLine(value7.GetValue(item)); // Prints "7"
Accessing the 8th Parameter and Beyond
Accessing the 8th tuple parameter and beyond is more complicated, as Vasilios found out while trying to use reflection to access the values stored in the Rest
field.
In the following code sample, we see that there is no Item8
. Instead we need to get the value of the Rest
field, which contains items 8, 9, and 10, and then get the first item, Item1
, which corresponds to item 8.
var item = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var value8a = item.GetType().GetField("Item8");
Console.Out.WriteLine(value8a == null); // Prints "True"
var restField = item.GetType().GetField("Rest");
var rest = restField.GetValue(item);
var value8b = rest.GetType().GetField("Item1");
Console.Out.WriteLine(value8b.GetValue(rest)); // Prints "8"
Vasilios ran into trouble by trying to access Item1
on restField
instead of rest
. restField
is of type FieldInfo
, whereas rest
is of type ValueTuple<T1, T2, T3>
.
Iterating through ValueTuple parameters
Finally, you may want to enumerate all parameters on a ValueTuple. To handle arbitrarily large ValueTuples, you need to recursively handle the Rest
field.
In the following code sample, we create a queue to iterate through the chain of ValueTuple Rest
fields. You could also implement EnumerateValueTuple
using recursion.
var item = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
foreach(var value in EnumerateValueTuple(item))
Console.Out.WriteLine(value); // Prints "1 2 3 4 5 6 7 8 9 10"
static IEnumerable<object> EnumerateValueTuple(object valueTuple)
{
var tuples = new Queue<object>();
tuples.Enqueue(valueTuple);
while(tuples.Count > 0 && tuples.Dequeue() is object tuple)
{
foreach(var field in tuple.GetType().GetFields())
{
if(field.Name == "Rest")
tuples.Enqueue(field.GetValue(tuple));
else
yield return field.GetValue(tuple);
}
}
}
Accessing Tuples at Runtime without Reflection
Update (3rd February 2018): Airbreather points out on Reddit that as of .NET Core 2.0 and .NET Framework 4.7.1, it is now possible to access the tuple values dynamically at runtime without using reflection.
This is achieved by importing System.Runtime.CompilerServices
and casting the tuple to ITuple
, which provides an indexer and a Length
property:
using System.Runtime.CompilerServices;
var item = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var tuple = item as ITuple;
for(int i = 0; i < tuple.Length; i++)
Console.Out.WriteLine(tuple[i]); // Prints "1 2 3 4 5 6 7 8 9 10"
If you're targeting .NET Core 2.0+ or .NET Framework 4.7.1+, then this is a much better way to dynamically access the tuple values. Unfortunately, ITuple
is not part of .NET Standard 2.0 and therefore, cannot be used in libraries targeting .NET Standard.