Creating classes is bread and butter when it comes to C# programming. But creating a new type (class) comes with a cost, like everything in programming. However, there are situations when we need something simpler and more disposable – something for one time use, really. In this case, we should consider using the anonymous type. This will be the main point of this blog post.
If you want to download the code, it can be found at: https://github.com/Nivo1985/AdvancedCSharp. The project names match the blog post topic.
Starting simple
The official definition is always a good place to start learning something. In this case the definition goes like this: ” Anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without having to explicitly define a type first. The type of name is generated by the compiler and is not available at the source code level. The type of each property is inferred by the compiler”.
Using the described technique is super easy. Let’s take a look.
1 2
var inst1 = new { id =1, Name = "Karol"}; Console.WriteLine(inst1.ToString());
This line of code is all we need to demonstrate what the anonymous type is. You can think of it as of creating an object without a class. Looks simple, but how does it work under the hood? To find out, we will take a look at the Low-Level C# code. It can be found in the IL Viewer Tab in Rider IDE. Over there the low-Level C# looks like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Decompiled with JetBrains decompiler // Type: Program // Assembly: AnonymousTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 66379900-E178-405A-9398-7BF076623864 // Assembly location: C:\SH\Blog\AdvancedCSharp\AdvancedCSharp\AnonymousTypes\bin\Debug\net7.0\AnonymousTypes.dll // Compiler-generated code is shown using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal class Program { private static void <Main>$(string[] args) { var data = new <>f__AnonymousType0<int, string>(1, "Karol"); Console.WriteLine(data.ToString()); } public Program() { base..ctor(); } }
This is where the fun begins
The code should be mostly understandable. The thing I want to focus on is this f__AnonymousType0. What is it? It is just after the new keyword, so we are creating a new instance of something. But instance of what? We haven’t created any class. Or have we? Just like in that movie meme. This is where the fun begins.
To look at what f__AnonymousType0 is, we need to decompile it. There are some options to do it. In my experience the best one is a free tool called dotPeek. It can be found at https://www.jetbrains.com/decompiler/download/#section=web-installer. Once we install and open the tool all we need to do is to open the dll we want to decompile. After opening dll you will see something like that.
If you double, click the place I marked the C# file to see it.
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
// Decompiled with JetBrains decompiler // Type: <>f__AnonymousType0`2 // Assembly: AnonymousTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 7DC05E5F-CFCF-410F-B80F-203DB503F4E9 // Assembly location: C:\SH\Blog\AdvancedCSharp\AdvancedCSharp\AnonymousTypes\bin\Debug\net7.0\AnonymousTypes.dll using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; #nullable enable [CompilerGenerated] [DebuggerDisplay("\\{ id = {id}, Name = {Name} }", Type = "<Anonymous Type>")] internal sealed class \u003C\u003Ef__AnonymousType0<\u003Cid\u003Ej__TPar, \u003CName\u003Ej__TPar> { [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly #nullable disable \u003Cid\u003Ej__TPar \u003Cid\u003Ei__Field; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly \u003CName\u003Ej__TPar \u003CName\u003Ei__Field; public \u003Cid\u003Ej__TPar id => this.\u003Cid\u003Ei__Field; public \u003CName\u003Ej__TPar Name => this.\u003CName\u003Ei__Field; [DebuggerHidden] public \u003C\u003Ef__AnonymousType0(\u003Cid\u003Ej__TPar id, \u003CName\u003Ej__TPar Name) { // ISSUE: reference to a compiler-generated field this.\u003Cid\u003Ei__Field = id; // ISSUE: reference to a compiler-generated field this.\u003CName\u003Ei__Field = Name; } [DebuggerHidden] public override bool Equals(object value) { var data = value as \u003C\u003Ef__AnonymousType0<\u003Cid\u003Ej__TPar, \u003CName\u003Ej__TPar>; if (this == data) return true; // ISSUE: reference to a compiler-generated field // ISSUE: reference to a compiler-generated field // ISSUE: reference to a compiler-generated field // ISSUE: reference to a compiler-generated field return data != null && EqualityComparer<\u003Cid\u003Ej__TPar>.Default.Equals(this.\u003Cid\u003Ei__Field, data.\u003Cid\u003Ei__Field) && EqualityComparer<\u003CName\u003Ej__TPar>.Default.Equals(this.\u003CName\u003Ei__Field, data.\u003CName\u003Ei__Field); } [DebuggerHidden] public override int GetHashCode() => (1139200652 * -1521134295 + EqualityComparer<\u003Cid\u003Ej__TPar>.Default.GetHashCode(this.\u003Cid\u003Ei__Field)) * -1521134295 + EqualityComparer<\u003CName\u003Ej__TPar>.Default.GetHashCode(this.\u003CName\u003Ei__Field); [DebuggerHidden] public override #nullable enable string ToString() { object[] objArray = new object[2]; // ISSUE: reference to a compiler-generated field \u003Cid\u003Ej__TPar idIField = this.\u003Cid\u003Ei__Field; ref \u003Cid\u003Ej__TPar local1 = ref idIField; objArray[0] = (object) ((object) local1 != null ? local1.ToString() : (string) null); // ISSUE: reference to a compiler-generated field \u003CName\u003Ej__TPar nameIField = this.\u003CName\u003Ei__Field; ref \u003CName\u003Ej__TPar local2 = ref nameIField; objArray[1] = (object) ((object) local2 != null ? local2.ToString() : (string) null); return string.Format((IFormatProvider) null, "{{ id = {0}, Name = {1} }}", objArray); } }
As you can see, this code looks much more challenging than the low-Level one. But thanks to that it allows us to look at what is actually going on behind the scenes.
At the very top we can see what f__AnonymousType0 is - it is an internal generic sealed class. It has two parameters, just like the number of fields in the object we created this way. It has the constructor, and even though we haven’t used is explicitly, it is used in internal processes.
Beneath the constructor we can see three overwritten methods. We are interested in two of them: Equals and ToString. Equals is value- based. In the case of anonymous referenced types, this makes absolute sense. As for the ToString, it combines all the object fields and values pairs into one string. We should see it in action. To do it I will add a bit more code.
1 2 3 4 5 6 7
var inst1 = new { id =1, Name = "Karol"}; var inst2 = new { id =1, Name = "Karol"}; Console.WriteLine(inst1.ToString()); Console.WriteLine("inst1.Equals(inst2)"); Console.WriteLine(inst1.Equals(inst2)); Console.WriteLine("inst1 == inst2"); Console.WriteLine(inst1 == inst2);
We have created a second object, with same the fields and values. After that, we compared it in two ways. Let’s see what the results are.
As we can see, comparing using the method returns true and with == it gave us false. The second result is 100% expectable. Those are objects and we are comparing the references. But why is the first result true? The answer is in the IL Viewer.
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
// Decompiled with JetBrains decompiler // Type: Program // Assembly: AnonymousTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: 6C8CBCCC-C691-43EC-BAC2-B56AD92E8EE5 // Assembly location: C:\SH\Blog\AdvancedCSharp\AdvancedCSharp\AnonymousTypes\bin\Debug\net7.0\AnonymousTypes.dll // Compiler-generated code is shown using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal class Program { private static void <Main>$(string[] args) { var data1 = new <>f__AnonymousType0<int, string>(1, "Karol"); var data2 = new <>f__AnonymousType0<int, string>(1, "Karol"); Console.WriteLine(data1.ToString()); Console.WriteLine("inst1.Equals(inst2)"); Console.WriteLine(data1.Equals((object) data2)); Console.WriteLine("inst1 == inst2"); Console.WriteLine(data1 == data2); } public Program() { base..ctor(); } }
The compiler is “smart”. If it detects matching anonymous types, only one class will be generated. That is why comparing value gave us true. Mystery solved.
The with keyword
I want to use the occasion of us talking about anonymous types to discuss with keyword. This keyword comes handy in the right situation. And now the situation is just right. The thing about anonymous objects is that the fields are read-only. So, we can’t change it. And if we really need to, we have to create a new object based on the original one, with changes made in the process.
This is the moment when with keyword comes into play. This keyword allows us to do just what we described. The code and the result look like this:
1 2 3 4
var inst3 = new { id = 1, value = 10 }; var inst4 = inst3 with { value = 12 }; Console.WriteLine(inst3.ToString()); Console.WriteLine(inst4.ToString());
It shows us two things. First, the new object was created based on an object and has a property changed. The original object was not changed which is useful., especially when it comes to large objects.
Summary
This was the fifth article in the advanced C# series. We have talked about anonymous types and have taken a deep dive into this subject. Thanks to that, we know when it’s the right situation to use it. We also learned a lot of tricks and tips.
If you have any questions, please drop me a line at karol.rogowski@softwarehut.com.
Till next time. Keep coding.