Practical .NET
C# 9: Value Objects and Simpler Code
C# 9 gives you a better way to create value objects and some simpler code to use while doing it. But even if you don't care about value objects, the new keyword has some cool changes.
I won't say that, all by itself, C# 9 is worth the migration to .NET 5 (I might make that claim about C# 8 and .NET Core 3.x, though). The new version of C# 9 is more than just a nice feature of .NET 5, though, and here are my favorite new features.
Immutable Objects
I'm a big fan of domain driven design. One of the key concepts of that approach are what are called value objects: objects that are considered identical because they have the same values in their properties, not because they share a primary key or location in memory. An address is a good example of a value object: two addresses are the same if they have identical city/street/etc. even if they're from two different customers and one is a "Shipping" address while the other is a "Billing" address.
Structs (and other value types) work that way when compared, but with structs, assigning a value from one struct to another copies the data (it's different with classes: assigning one variable to another just copies pointers around and both variables end up pointing to the same object in memory rather than getting their own copies of the data).
The problem with creating a copy of a value object is that the two copies can have separate changes made to them -- what started off as two identical objects drift apart. As a result, value objects are often made to be immutable, something that requires a fair amount of code to set up in .NET. That actually makes copying the data a bigger problem: You now have data that is guaranteed to be identical taking up twice as much space as necessary.
But, in C# 9, you can just create a record and get a value object that does everything you want. Here's an immutable Address value object:
public record Address
{
public string Street { get; }
public string City { get; }
public Address()
{
this.Street = string.Empty;
this.City = string.Empty;
}
public Address(string Street, string City)
{
this.Street = Street;
this.City = City;
}
}
And here's how I can create two Address objects:
Address addr1 = new Address("Ridout", "London");
Address addr2 = new Address("Ridout", "London");
While this looks like I'm creating a reference type, when you compare the two record objects, they're compared like value types -- it's the value of the properties that matter (provided the two objects are of the same type). So, in this code, the test will pass because the Street and City properties have the same values in both of the objects:
if (addr1 == addr2) {
//….addresses are the same
}
On the other hand, when you assign variables, records work like reference types: pointers are moved around but no data is copied. In this code, the addr1 and addr2 variables both point at the same object in memory:
Address addr1 = new Address("Ridout", "London");
Address addr2 = addr1;
Like classes, records support inheritance and interfaces. Records also throw in a useful ToString method. Calling the ToString method on my Address object produces this, for example:
Address { Street = Ridout , City = London }
Controlling and Defining Properties in Records
As with any property that's declared as read-only (like my Street and City properties), this code won't work because my properties, as defined, can only be set in the constructor:
Address addr = new Address {Street = "Ridout", City = "London" }
In C# 9, you can now indicate that a readonly property can be set when an object is instantiated by marking the property as "init"able, like this:
public record Address
{
public string Street { get; init; }
public string City { get; init; }
This code that sets those properties as the object is instantiated will now work:
Address addr = new Address {Street = "Ridout", City = "London" }
If I want, when assigning a record variable I can force a copy to be created by using the with
keyword. Typically, I'll do this because I want to change the value of some property as part of the copy. This code, for example, creates a new Address but with the value in City changed:
Address newAddress = addr1 with { City = "Goderich" }
I can also extract my record's properties into individual variables. This code moves my Street and City properties into two string variables:
var (street, city) = addr;
MessageBox.Show("The city is " + city);
This is called "deconstructing" a record and, if you're unhappy with the way that your record is being deconstructed, you can write your own Deconstruct method. A Deconstruct method accepts an out parameter for each individual variable that will be returned and then, in the method's body, assigns values to those parameters (typically, from the properties in the record but you can do whatever you want).
This code, for example, overrides the default Deconstruct method and returns only the City property:
public void Deconstruct(out string city)
{
city = City;
}
But I'm doing too much typing. Provided I'm willing to give up my constructors, I can define my Address value object just by listing its properties when I declare it, like this (note the semicolon at the end of the statement):
public record Address(string Street, string City);
The syntax looks like the result of collapsing the declaration of the class and the parameter list from the record's constructor into a single line. This syntax doesn't stop you from adding methods, a constructor or Deconstruct method, or even redefining the properties with your own code.
This code, for example, adds a body to the record and makes the Street property writeable:
public record Address(string Street, string City)
{
public string Street {get; set; }
}
Simpler Newing Up
As long as we're talking about reducing the amount of typing, my other favorite change are the ones around the new
keyword.
In my previous examples, when creating my Address object, I repeated the record name on either side of the equals sign. This supports declaring the variable as a different type than the class or record (e.g. declaring the variable using an interface or a base class). But the reality is that, much of the time, the name on both sides of the equal sign is the same:
Address addr = new Address("Ridout", "London");
In these cases, in C# 9, you can omit the class name on the right-hand side and just collapse the rest of the statement to the left. This code does exactly what my previous statement did:
Address addr = new("Ridout", "London");
Microsoft calls this "target typing" and it works almost everywhere you'd want it to. I can use this new syntax both when creating a typed collection and when adding items to it, as this code demonstrates:
List<Address> addrs = new();
addrs.Add(new( "Ridout", "London" ), new( "Shore", "London" ), new( "St. Patrick", "Goderich" ));
As you can see, target typing really pays off when you're loading multiple copies of the same type into a collection.
Target typing also works if you're passing a new object to a method's typed parameter, as in this example:
PrintAddress (new("Ridout", "London"));
…
public void PrintAddress(Address addr)
{
There's more to talk about in C# 9 (including enhancements to pattern matching -- a topic for a post all by itself). These are, however, are ones that I'll use all the time.
Maybe it is a reason to migrate, after all.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.