BLOG
14 March 2023

Advanced C#: Using ValueTuple

tech

In the previous blog post we have been talking about anonymous types. This time we will build upon this idea and show different approaches to a similar problem. I’m talking about Tuple.  

The book definition of it goes as follows: The tuples feature provides concise syntax to group multiple data elements in a lightweight data structure. 

If you want to download the code or take a look at it, it can be found at GitHub. The project names match the blog post topic. In this case, as we are working on top of anonymous types, this is the project we will be working on.  

Starting easy 

Tuple takes slightly different approach to storing data compared to anonymous types. First of all, Tuple is a defined type and it is a value type as well. A generic (in most cases 😊) struct. Its characteristics make it different from classes and anonymous types. As I believe it is best to discuss code while coding, let’s take a look on how to create a tuple.  

1 2 3 4 var order = Order.GetOrders().FirstOrDefault(); if (order != null) { var example1 = (OrderId: order.Id, OrderName: order.Name, ItemsCount: order.Items!.Count);

And here it is. This is how we create a tuple. It can be done using a set of parentheses. In this particular case we have created a tuple with three fields. To see what is happening behind the scenes we can go to the IL Viewer - yes I’m using JB Rider - and take a look at Low-Level C#. 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [CompilerGenerated] internal class Program { private static void <Main>$(string[] args) { Order order = Order.GetOrders().FirstOrDefault<Order>(); if (order != null) { ValueTuple<int, string, int> valueTuple = new ValueTuple<int, string, int>(order.Id, order.Name, order.Items.Count); } } public Program() { base..ctor(); } }

The code looks very much like our original C# code. The main difference for us is the fact that we can see the type we are actually using. It is ValueTuple. If you want to fully understand the code you are working with, I strongly recommend to go to the actual IL View instead of the low-level code. By doing so we will be able to look at the code and see what is going on. What should be interesting for us is the fact that we can see that it is actually a value type.  

1 2 3 4 5 6 .locals init ( [0] class AnonymousTypes.Utils.Order order, [1] int32 i, [2] bool V_2, [3] valuetype [System.Runtime]System.ValueTuple`3<int32, string, int32> example1 )
1 2 3 4 5 6 7 8 9 IL_0014: ldloca.s example1 IL_0016: ldloc.0 // order IL_0017: callvirt instance int32 AnonymousTypes.Utils.Order::get_Id() IL_001c: ldloc.0 // order IL_001d: callvirt instance string AnonymousTypes.Utils.Order::get_Name() IL_0022: ldloc.0 // order IL_0023: callvirt instance class [System.Collections]System.Collections.Generic.List`1<class AnonymousTypes.Utils.Item> AnonymousTypes.Utils.Order::get_Items() IL_0028: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1<class AnonymousTypes.Utils.Item>::get_Count() IL_002d: call instance void valuetype [System.Runtime]System.ValueTuple`3<int32, string, int32>::.ctor(!0/*int32*/, !1/*string*/, !2/*int32*/)

Now that we have dug super deep, I just want to focus all our attention on ValueTuple definition. The code for the type can be found in the .net source code in the ValueTuple.cs file. When we find and explore it, the definition can be found.  

1 2 3 4 5 6 7 8 9 10 11 12 13 /// <summary> /// Represents a 3-tuple, or triple, as a value type. /// </summary> /// <typeparam name="T1">The type of the tuple's first component.</typeparam> /// <typeparam name="T2">The type of the tuple's second component.</typeparam> /// <typeparam name="T3">The type of the tuple's third component.</typeparam> [Serializable] [StructLayout(LayoutKind.Auto)] [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] public struct ValueTuple<T1, T2, T3> : IEquatable<ValueTuple<T1, T2, T3>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2, T3>>, IValueTupleInternal, ITuple

Try playing around with all the elements composing ValueTuple structure. It is about time to run the code and see what was created. The best way to do it is in the Debug node with breakpoint after the tuple variable.

Now we can see the full picture. The variable is of generic type ValueTuple<int,string, int>, exactly what we expected. This was the most basic example and it worked this time, so to get to know this type a bit better.  

(Not) Read-Only 

Unlike the anonymous types, this one had its properties set as read-only. But ValueTuple variables can be modified, so there’s no problem there. In this example, we are working with the tuple from previous code. After tuple is created, we will change the value of the ItemCount field - I assure you that it is going to work without issues. Let’s take a closer look. 

1 2 3 4 5 6 7 8 9 10 11 var order = Order.GetOrders().FirstOrDefault(); if (order != null) { var example1 = (OrderId: order.Id, OrderName: order.Name, ItemsCount: order.Items!.Count); Console.WriteLine("example1"); Console.WriteLine(example1); example1.ItemsCount++; Console.WriteLine(example1); example1.ItemsCount--; }

Equal or not? 

Being a value type has its pros and cons. One of which is the equality. ValueTuple being one of this value types falls into that pattern - if the variable fields values match, the variables are equal. Here’s an example: 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 var order = Order.GetOrders().FirstOrDefault(); if (order != null) { var example1 = (OrderId: order.Id, OrderName: order.Name, ItemsCount: order.Items!.Count); Console.WriteLine("example1"); Console.WriteLine(example1); example1.ItemsCount++; Console.WriteLine(example1); example1.ItemsCount--; var example2 = (OrderId: order.Id,OrderName: order.Name,ItemsCount: order.Items!.Count); Console.WriteLine("example1 == example2"); Console.WriteLine(example1 == example2);

Serialization 

Another major difference becomes visible when it comes to the serialization. ValueTuple exposes its values as a fields, not properties, just like anonymous types. This makes a huge difference when it comes to the serialization. The code bellow shows the difference between serializing this two mentioned types. 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var order = Order.GetOrders().FirstOrDefault(); if (order != null) { var example1 = (OrderId: order.Id, OrderName: order.Name, ItemsCount: order.Items!.Count); Console.WriteLine("example1"); Console.WriteLine(example1); example1.ItemsCount++; Console.WriteLine(example1); example1.ItemsCount--; var example2 = (OrderId: order.Id,OrderName: order.Name,ItemsCount: order.Items!.Count); Console.WriteLine("example1 == example2"); Console.WriteLine(example1 == example2); var jsonTP = JsonSerializer.Serialize(example1); Console.WriteLine("Tuple serialized"); Console.WriteLine(jsonTP); var exampleAT = new { OrderId = order.Id, OrderName = order.Name, ItemsCount = order.Items!.Count }; var jsonAT = JsonSerializer.Serialize(exampleAT); Console.WriteLine("Tuple serialized"); Console.WriteLine(jsonAT);

Yes, ValueTuple Can 

Unlike anonymous type ValueTuple is strongly typed. Generic, but still strongly typed. This indicates two very important ideas. One is that it can be used as an input and output in these methods. Two, it can be extended. We will take a look at both in the code example below. 

1 2 3 4 5 6 7 8 9 namespace AnonymousTypes.Utils; public static class TupleExtensions { public static string ToOneString(this ValueTuple<int,string, string> input) { return $"ID: {input.Item1.ToString()}, Name: {input.Item2}, Number of Items: {input.Item3}"; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var order = Order.GetOrders().FirstOrDefault(); if (order != null) { var example1 = (OrderId: order.Id, OrderName: order.Name, ItemsCount: order.Items!.Count); Console.WriteLine("example1"); Console.WriteLine(example1); example1.ItemsCount++; Console.WriteLine(example1); example1.ItemsCount--; var example2 = (OrderId: order.Id,OrderName: order.Name,ItemsCount: order.Items!.Count); Console.WriteLine("example1 == example2"); Console.WriteLine(example1 == example2); var jsonTP = JsonSerializer.Serialize(example1); Console.WriteLine("Tuple serialized"); Console.WriteLine(jsonTP); var exampleAT = new { OrderId = order.Id, OrderName = order.Name, ItemsCount = order.Items!.Count }; var jsonAT = JsonSerializer.Serialize(exampleAT); Console.WriteLine("Anonymus serialized"); Console.WriteLine(jsonAT); Console.WriteLine(TupleMethod(example1).ToOneString()); } // Tuples END var i = 1; static ValueTuple<int,string, string> TupleMethod((int Input_OrderId, string Input_OrderName, int Input_ItemsCount) input) { return (input.Input_OrderId, input.Input_OrderName, input.Input_ItemsCount.ToString()); }

Summary  

This was the sixth article in the Advanced C# series. You know now that Tuple cannot fully make an educated decision when creating “temporal” groups of data. I hope this complete view of both possible options helps you.   

If you have any questions, please drop me a line at karol.rogowski@softwarehut.com. 

Till next time. Keep coding! 



Author
Karol Rogowski
Head Of Engineering

Working in IT since 2009. Currently working as Head of Engineering at SoftwareHut and as an academic teacher at Białystok Technical University. Co-founder of meet.js Białystok. Book and articles author. Father, husband, huge H.P. Lovecraft fan and terrible poker player.