Appendix A

This appendix describes different C# features that we use or are related to topics we use in the book. I cannot cover all C# features in an appendix, but I did my best to pick the most relevant ones.

We are covering the following:

  • Older C# features covering C# 1 to 8
  • What’s new in .NET 5 and C# 9? covering features from C# 9
  • What’s new in .NET 6 and C# 10? covering features from C# 10

Older C# features

This section covers a list of C# features that are useful, less known, or I want to make sure you are aware of since we are leveraging or mentioning them in the book.

The null-coalescing operator (C# 2.0)

The null-coalescing (??) operator is a binary operator written using the following syntax: result = left ?? right. It expresses to use the right value when the left value is null. Otherwise, the left value is used.

Here is a console application using the null-coalescing operator:

Console.WriteLine(ValueOrDefault(default, "Default value"));
Console.WriteLine(ValueOrDefault("Some value", "Default value"));
static string ValueOrDefault(string? value, string defaultValue)
{
    return value ?? defaultValue;
}

The ValueOrDefault method returns defaultValue when value is null; otherwise, it returns value. Executing that program outputs the following:

Default value
Some value

The null-coalescing (??) operator is very convenient as it saves us from writing code like the following equivalent method:

static string ValueOrDefaultPlain(string? value, string defaultValue)
{
    if (value == null)
    {
        return defaultValue;
    }
    return value;
}

Interesting Fact

C# 2.0 is also the version they added generics, which were a very welcome addition. Try to imagine C# without generics.

Expression-bodied member (C# 6-7)

Expression-bodied members allow us to write an expression (a line of code) after the arrow operator (=>) instead of the body of that member (delimited by {}). We can write methods, properties, constructors, finalizers, and indexers this way.

Here is a small program that leverages this capability:

Console.WriteLine(new Restaurant("The Cool Place"));
Console.WriteLine(new Restaurant("The Even Cooler Place"));
public class Restaurant
{
    public readonly string _name;
    public Restaurant(string name)
        => _name = name;
    public string Name => _name; // read-only property
    public override string ToString()
        => $"Restaurant: {Name}";
}

Executing the program yields:

Restaurant: The Cool Place
Restaurant: The Even Cooler Place

The equivalent with bodies would be the following code:

public class RestaurantWithBody
{
    public readonly string _name;
    public RestaurantWithBody(string name)
    {
        _name = name;
    }
    public string Name
    {
        get
        {
            return _name;
        }
    }
    public override string ToString()
    {
        return $"Restaurant: {Name}";
    }
}

As we can see from the preceding example, expression-bodied members allow us to make the code denser with less noise (less {}).

Note

I find that expression-bodied members reduce readability when the right-hand expression is complex. I rarely use expression-bodied constructors and finalizers as I find they make the code harder to read. However, read-only properties and methods can benefit from this construct as long as the right-hand expression is simple.

Throw expressions (C# 7.0)

This feature allows us to use the throw statement as an expression, giving us the possibility to throw exceptions on the right side of the null-coalescing operator (??).

The good old-fashioned way of writing a guard clause, before throw expressions, was as follows:

public HomeController(IHomeService homeService)
{
    if (homeService == null)
    {
        throw new ArgumentNullException(nameof(homeService));
    }
    _homeService = homeService;
}

In the preceding code, we first check for null, and if homeService is null, we throw an ArgumentNullException; otherwise, we assign the value to the field _homeService.

Now, with throw expressions, we can write the preceding code as a one-liner instead:

public HomeController(IHomeService homeService)
{
    _homeService = homeService ?? throw new ArgumentNullException(nameof(homeService));
}

Before C# 7.0, we could not throw an exception from the right side (it was a statement), but now we can (it is an expression).

Note

From C# 10 onward, we can now write guards using the static ThrowIfNull method of the ArgumentNullException class, like this:

public HomeController(IHomeService homeService)
{
    ArgumentNullException.ThrowIfNull(homeService);
    _homeService = homeService;
}

This makes the intent a little more explicit but does not assign the value to the field, which is less than ideal for a constructor guard. If the objective is only to validate for nulls, like in a method, this new method can be handy.

Tuples (C# 7.0+)

A tuple is a type that allows returning multiple values from a method or stores multiple values in a variable without declaring a type and without using the dynamic type. Since C# 7.0, tuple support has greatly improved.

Note

Using dynamic objects is OK in some cases, but beware that it could reduce performance and increase the number of runtime exceptions thrown due to the lack of strong types. Moreover, dynamic objects bring limited tooling support, making it harder to discover what an object can do; it is more error-prone than a strong type, there is no type checking, no auto-completion, and no compiler validation. Compile-time errors can be fixed right away, without the need to wait for them to arise during runtime, or worse, be reported by a user.

The C# language adds syntactic sugar regarding tuples that makes the code clearer and easier to read. Microsoft calls that lightweight syntax.

If you’ve used the Tuple classes before, you know that Tuple members are accessed through Item1, Item2, and ItemN properties. The ValueTuple struct also exposes similar fields. This newer syntax is built on top of the ValueTuple struct and allows us to eliminate those generic names from our codebase and replace them with meaningful user-defined ones. From now on, when referring to tuples, I refer to C# tuples, or more precisely an instance of ValueTuple. If you’ve never heard of tuples, we explore them right away.

Let’s jump right into a few samples, coded as xUnit tests. The first shows how we can create an unnamed tuple and access its fields using Item1, Item2, and ItemN, which we talked about earlier:

[Fact]
public void Unnamed()
{
    var unnamed = ("some", "value", 322);
    Assert.Equal("some", unnamed.Item1);
    Assert.Equal("value", unnamed.Item2);
    Assert.Equal(322, unnamed.Item3);
}

Then, we can create a named tuple—very useful if you don’t like those 1, 2, 3 fields:

[Fact]
public void Named()
{
    var named = (name: "Foo", age: 23);
    Assert.Equal("Foo", named.name);
    Assert.Equal(23, named.age);
}

Since the compiler does most of the naming, and even if IntelliSense is not showing it to you, we can still access those 1, 2, 3 fields:

[Fact]
public void Named_equals_Unnamed()
{
    var named = (name: "Foo", age: 23);
    Assert.Equal(named.name, named.Item1);
    Assert.Equal(named.age, named.Item2);
}

Note

If you loaded the whole Git repository, a Visual Studio analyzer should tell you not to do this by underlining those members with red error-like squiggly lines because of the configuration I’ve made in the .editorconfig file, which instructs Visual Studio how to react to coding styles. In a default context, you should see a suggestion instead.

Moreover, we can create a named tuple using variables where names follow “magically”:

[Fact]
public void ProjectionInitializers()
{
    var name = "Foo";
    var age = 23;
    var projected = (name, age);
    Assert.Equal("Foo", projected.name);
    Assert.Equal(23, projected.age);
}

Since the values are stored in those 1, 2, 3 fields, and the programmer-friendly names are compiler-generated, equality is based on field order, not field name. Partly due to that, comparing whether two tuples are equal is pretty straightforward:

[Fact]
public void TuplesEquality()
{
    var named1 = (name: "Foo", age: 23);
    var named2 = (name: "Foo", age: 23);
    var namedDifferently = (Whatever: "Foo", bar: 23);
    var unnamed1 = ("Foo", 23);
    var unnamed2 = ("Foo", 23);
    Assert.Equal(named1, unnamed1);
    Assert.Equal(named1, named2);
    Assert.Equal(unnamed1, unnamed2);
    Assert.Equal(named1, namedDifferently);
}

If you don’t like to access the tuple’s members using the dot (.) notation, we can also deconstruct them into variables:

[Fact]
public void Deconstruction()
{
    var tuple = (name: "Foo", age: 23);
    var (name, age) = tuple;
    Assert.Equal("Foo", name);
    Assert.Equal(23, age);
}

Methods can also return tuples and can be used the same way that we saw in previous examples:

[Fact]
public void MethodReturnValue()
{
    var tuple1 = CreateTuple1();
    var tuple2 = CreateTuple2();
    Assert.Equal(tuple1, tuple2);
    static (string name, int age) CreateTuple1()
    {
        return (name: "Foo", age: 23);
    }
    static (string name, int age) CreateTuple2()
        => (name: "Foo", age: 23);
}

Note

The methods are local functions, but the same applies to normal methods as well.

To conclude on tuples, I suggest avoiding them on public APIs that are exported (a shared library, for example). However, I find they come in handy internally to code helpers without creating a class that holds only data and is used once or a few times.

I think that tuples are a great addition to .NET, but I prefer fully defined types on public APIs for many reasons. The first reason is encapsulation; tuple members are fields, which breaks encapsulation. Then, accurately naming classes that are part of an API (contract/interface) is essential.

Tip

When you can’t find an exhaustive name for a type, the chances are that some business requirements are blurry, what is under development is not exactly what is needed, or the domain language is not clear. When that happens, try to word a clear statement about what you are trying to accomplish and if you still can’t find a name, try to rethink that API.

For example, “I want to calculate the sales tax rate of the specified product” could yield a CalculateSalesTaxRate(...) method in a Product class or a CalculateSalesTaxRate(Product product, ...) in another class.

An excellent alternative to tuples for public APIs is record classes, keeping additional code minimal.

Default literal expressions (C# 7.1)

Default literal expressions were introduced in C# 7.1 and allow us to reduce the amount of code required to use default value expressions.

Previously, we needed to write this:

string input = default(string);

Or this:

var input = default(string);

Now, we can write this:

string input = default;

It can be very useful for optional parameters, like this:

public void SomeMethod(string input1, string input2 = default)
{
    // …
}

In the method defined in the preceding code block, we can pass one or two arguments to the method. When we omit the input2 parameter, it is instantiated to default(string), which is null.

We can use default literal expressions instead, which allow us to do the following:

  • Initialize a variable to its default value.
  • Set the default value of an optional method parameter.
  • Provide a default argument value to a method call.
  • Return a default value in a return statement or an expression-bodied member (the arrow => operator introduced in C# 6 and 7).

Here is an example covering those use cases:

public class DefaultLiteralExpression<T>
{
    public void Execute()
    {
        // Initialize a variable to its default value
        T? myVariable = default;
        var defaultResult1 = SomeMethod();
        // Provide a default argument value to a method call
        var defaultResult2 = SomeOtherMethod(myVariable, default);
    }
    // Set the default value of an optional method parameter
    public object? SomeMethod(T? input = default)
    {
        // Return a default value in a return statement
        return default;
    }
    // Return a default value in an expression-bodied member
    public object? SomeOtherMethod(T? input, int i) => default;
}

We used the generic T type parameter in the examples, but that could be any type. The default literal expressions become handy with complex generic types such as Func<T>, Func<T1, T2>, or tuples.

Here is a good example of how simple it is to return a tuple and return the default values of its three components using a default literal expression:

public (object, string, bool) MethodThatReturnATuple()
{
    return default;
}

It is important to note that the default value of reference types (classes) is null, but the default of value types (struct) is an instance of that struct with all its fields initialized to their respective default value. C# 10 introduces the ability to define a default parameterless constructor to value types, which initializes that struct’s default instance when using the default keyword, overriding the preceding assertion about default fields. Moreover, many built-in types have custom default values; for example, the default for numeric types and enum is 0 while a bool is false.

Switch expressions (C# 8)

This feature was introduced in C# 8 and is named switch expressions. Previously, we had to write this (code taken from the Strategy pattern code sample from Chapter 6, Understanding the Strategy, Abstract Factory, and Singleton Design Patterns):

string output = default;
switch (input)
{
    case "1":
        output = PrintCollection();
        break;
    case "2":
        output = SortData();
        break;
    case "3":
        output = SetSortAsc();
        break;
    case "4":
        output = SetSortDesc();
        break;
    case "0":
        output = "Exiting";
        break;
    default:
        output = "Invalid input!";
        break;
}

Now, we can write this:

var output = input switch
{
    "1" => PrintCollection(),
    "2" => SortData(),
    "3" => SetSortAsc(),
    "4" => SetSortDesc(),
    "0" => "Exiting",
    _   => "Invalid input!"
};

That makes the code shorter and simpler. Once you get used to it, I find this new way even easier to read. You can think about a switch expression as a switch that returns a value.

Note

Switch expressions also support pattern matching introduced in C# 7. C# received more pattern matching features in subsequent versions. We are not covering pattern matching here.

Discards (C# 7)

Discards were introduced in C# 7. In the following example (code taken from the GitHub repo associated with the Strategy pattern code sample from Chapter 6, Understanding the Strategy, Abstract Factory, and Singleton Design Patterns), the discard became the default case of the switch (see the highlighted line):

var output = input switch
{
    "1" => PrintCollection(),
    "2" => SortData(),
    "3" => SetSortAsc(),
    "4" => SetSortDesc(),
    "0" => "Exiting",
    _   => "Invalid input!"
};

Discards (_) are also useable in other scenarios. It is a special variable that cannot be used, a placeholder, like a variable that does not exist. Using discards doesn’t allocate memory for that variable, which helps optimize your application.

It is useful when deconstructing a tuple and to use only some of its members. In the following code, we keep the reference on the name field but discard age during the deconstruction:

var tuple = (name: "Foo", age: 23);
var (name, _) = tuple;
Console.WriteLine(name);

It is also very convenient when calling a method with an out parameter that you don’t want to use, for example:

if (bool.TryParse("true", out _))
{
    Console.WriteLine("true was parsable!");
}

In that last code block, we only want to do something if the input is a Boolean, but we do not use the Boolean value itself, which is a great scenario for a discard variable.

Async main (C# 7.1)

From C# 7.1 onward, a console application can have an async Main method, which is very convenient as more and more code is becoming asynchronous. This new feature allows the use of await directly in the Main() method, without any quirks.

Previously, the signature of the Main method had to fit one of the following:

public static void Main() { }
public static int Main() { }
public static void Main(string[] args) { }
public static int Main(string[] args) { }

Since C# 7.1, we can also use their async counterpart:

public static async Task Main() { }
public static async Task<int> Main() { }
public static async Task Main(string[] args) { }
public static async Task<int> Main(string[] args) { }

Now, we can create a console application that looks like this:

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Entering Main");
        var myService = new MyService();
        await myService.ExecuteAsync();
        Console.WriteLine("Exiting Main");
    }
}
public class MyService
{
    public Task ExecuteAsync()
    {
        Console.WriteLine("Inside MyService.ExecuteAsync()");
        return Task.CompletedTask;
    }
}

When executing the program, the result is as follows:

Entering Main
Inside MyService.ExecuteAsync()
Exiting Main

Nothing fancy, but it allows us to take advantage of the await/async language feature directly from the Main method.

Note

From .NET Core 1.0 to .NET 5, all types of applications start with a Main method (usually Program.Main), including ASP.NET Core web applications. This addition is very useful and well needed. The minimal hosting model for ASP.NET Core introduced in .NET 6 is built on top of top-level statements, introduced in .NET 5, and they make this construct implicit since the compiler generates the Program class and the Main method for us. It is still there, good to know, but chances are you won’t need to write that code manually.

User-defined conversion operators (C# 1)

User-defined conversion operators are user-defined functions crafted to convert one type to another implicitly or explicitly. Many built-in types offer such conversions, such as converting an int to a long without any cast or method call:

int var1 = 5;
long var2 = var1; // This is possible due to a class conversion operator

Next is an example of custom conversion. We convert a string to an instance of the SomeGenericClass<string> class without a cast:

using Xunit;
namespace ConversionOperator;
public class SomeGenericClass<T>
{
    public T? Value { get; set; }
    public static implicit operator SomeGenericClass<T>(T value)
    {
        return new SomeGenericClass<T>
        {
            Value = value
        };
    }
}

The SomeGenericClass<T> class defines a generic property named Value that can be set to any type. The highlighted code block is the conversion operator method, allowing conversion from the type T to SomeGenericClass<T> without a cast. Let’s look at the result next:

[Fact]
public void Value_should_be_set_implicitly()
{
    var value = "Test";
    SomeGenericClass<string> result = value;
    Assert.Equal("Test", result.Value);
}

That first test method uses the conversion operator we just examined to convert a string to an instance of the SomeGenericClass<string> class. We can also leverage that to cast a value (a float in this case) to a SomeGenericClass<float> class, like this:

[Fact]
public void Value_should_be_castable()
{
    var value = 0.5F;
    var result = (SomeGenericClass<float>)value;
    Assert.Equal(0.5F, result.Value);
    Assert.IsType<SomeGenericClass<float>>(result);
}

Conversion operators also work with methods, as the next test method will show you:

[Fact]
public void Value_should_be_set_implicitly_using_local_function()
{
    var result1 = GetValue("Test");
    Assert.IsType<SomeGenericClass<string>>(result1);
    Assert.Equal("Test", result1.Value);
    var result2 = GetValue(123);
    Assert.Equal(123, result2.Value);
    Assert.IsType<SomeGenericClass<int>>(result2);
    static SomeGenericClass<T> GetValue<T>(T value)
    {
        return value;
    }
}

The preceding code implicitly converts a string into a SomeGenericClass<string> object and an int into a SomeGenericClass<int> object. The highlighted line returns the value of type T as an instance of the SomeGenericClass<T> class directly; the conversion is implicit.

This is not the most important topic of the book, but if you were curious, this is how .NET does this kind of implicit conversion (like returning an instance of T instead of an ActionResult<T> in MVC controllers). Now you know that you can implement custom conversion operators in your classes too when you want that kind of behavior.

Local functions (C# 7) and a static local function (C# 8)

In the previous example, we used a static local function, new to C# 8, to demonstrate the class conversion operator.

Local functions are definable inside methods, constructors, property accessors, event accessors, anonymous methods, lambda expressions, finalizers, and other local functions. Those functions are private to their containing members. They are very useful for making the code more explicit and self-explanatory without polluting the class itself, keeping them in the consuming member’s scope. Local functions can access the declaring member’s variables and parameters, like this:

[Fact]
public void With_no_parameter_accessing_outer_scope()
{
    var x = 1;
    var y = 2;
    var z = Add();
    Assert.Equal(3, z);
    x = 2;
    y = 3;
    var n = Add();
    Assert.Equal(5, n);
    int Add()
    {
        return x + y;
    }
}

That is not the most robust function because the inner scope (inline function) depends on the outer scope (method variables x and y). Nonetheless, the code shows how a local function can access its parent scope’s members, which is necessary in some cases.

The following code block shows a mix of inline function scope (the y parameter) and outer scope (the x variable):

[Fact]
public void With_one_parameter_accessing_outer_scope()
{
    var x = 1;
    var z = Add(2);
    Assert.Equal(3, z);
    x = 2;
    var n = Add(3);
    Assert.Equal(5, n);
    int Add(int y)
    {
        return x + y;
    }
}

That block shows how to pass an argument and how the local function can still use its outer scope’s variables to alter its result. Now, if we want an independent function, decoupled from its outer scope, we could code the following instead:

[Fact]
public void With_two_parameters_not_accessing_outer_scope()
{
    var a = Add(1, 2);
    Assert.Equal(3, a);
    var b = Add(2, 3);
    Assert.Equal(5, b);
    int Add(int x, int y)
    {
        return x + y;
    }
}

This code is less error-prone than the other alternatives; the logic is contained in a smaller scope (the function scope), leading to an independent inline function. But it still allows someone to alter it later and to use the outer scope since there is nothing to tell the intent of limiting access to the outer scope, like this (some unwanted outer scope access):

[Fact]
public void With_two_parameters_accessing_outer_scope()
{
    var z = 5;
    var a = Add(1, 2);
    Assert.Equal(8, a);
    var b = Add(2, 3);
    Assert.Equal(10, b);
    int Add(int x, int y)
    {
        return x + y + z;
    }
}

To clarify that intent, we can leverage static local functions. They remove the option to access the enclosing scope variables and clearly state that intent with the static keyword. The following is the static equivalent of a previous function:

[Fact]
public void With_two_parameters()
{
    var a = Add(1, 2);
    Assert.Equal(3, a);
    var b = Add(2, 3);
    Assert.Equal(5, b);
    static int Add(int x, int y)
    {
        return x + y;
    }
}

Then, with that clear definition, the updated version could become the following instead, keeping the local function independent:

[Fact]
public void With_three_parameters()
{
    var c = 5;
    var a = Add(1, 2, c);
    Assert.Equal(8, a);
    var b = Add(2, 3, c);
    Assert.Equal(10, b);
    static int Add(int x, int y, int z)
    {
        return x + y + z;
    }
}

Nothing can stop someone from removing the static modifier, maybe a good code review, but at least no one can say that the intent was not clear enough since the following would not compile:

[Fact]
public void With_two_parameters_accessing_outer_scope()
{
    var z = 5;
    var a = Add(1, 2);
    Assert.Equal(8, a);
    var b = Add(2, 3);
    Assert.Equal(10, b);
    static int Add(int x, int y)
    {
        return x + y + z;
    }
}

Using the enclosing scope can be useful sometimes, but I prefer to avoid that whenever possible, for the same reason that I do my best to avoid global stuff: the code can become messier, faster.

To recap, we can create a local function by declaring it inside another supported member without specifying any access modifier (public, private, and so on). That function can access its declaring scope, expose parameters, and do almost everything a method can do, including being async and unsafe. Then comes C# 8, which adds the option to define a local function as static, blocking the access to its outer scope and clearly stating the intent of an independent, standalone, private local function.

What’s new in .NET 5 and C# 9?

In this section, we explore the following C# 9 features:

  • Top-level statements
  • Target-typed new expressions
  • Init-only properties
  • Record classes

We use top-level statements to simplify code samples, leading to one code file with less boilerplate code. Moreover, top-level statements are the building blocks of the .NET 6 minimal hosting model and minimal APIs. We dig into the new expressions, which allow creating new instances with less typing. The init-only properties are the backbone of the record classes used in multiple chapters and are foundational to the MVU example presented in Chapter 18, A Brief Look into Blazor.

Top-level statements

Starting from C# 9, it is possible to write statements before declaring namespaces and other members. Those statements are compiled to an emitted Program.Main method.

With top-level statements, a minimal .NET “Hello World” program now looks like this:

using System;
Console.WriteLine("Hello world!");

Unfortunately, we also need a project to run, so we have to create a .csproj file with the following content:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
        <OutputType>Exe</OutputType>
    </PropertyGroup>
</Project>

From there, we can use the .NET CLI to dotnet run the application.

Note

I left the TargetFramework as net5.0 because this is related to .NET 5. We revisit top-level statements in the What’s new in .NET 6 and C# 10? section.

We can also declare other members, like classes, and use them as in any other application. Classes must be declared after the top-level code. Be aware that the top-level statement code is not part of any namespace, and it is recommended to create classes in a namespace, so you should limit the number of declarations done in the Program.cs file to what is internal to its inner workings.

Top-level statements are a great feature for getting started with C# and writing code samples by cutting out boilerplate code.

Target-typed new expressions

Target-typed new expressions are a new way of initializing types. C# 3 introduced the var keyword back in the day, which became very handy to work with generic types, LINQ return values, and more (I remember embracing that new construct with joy).

This new C# feature does the opposite of the var keyword by letting us call the constructor of a known type, like this:

List<string> list1 = new();
List<string> list2 = new(10);
List<string> list3 = new(capacity: 10);
var obj = new MyClass(new());
AnotherClass anotherObj = new() { Name = "My Name" };
public class MyClass {
    public MyClass(AnotherClass property)
        => Property = property;
    public AnotherClass Property { get; }
}
public class AnotherClass {
    public string? Name { get; init; }
}

The first highlight shows the ability to create new objects when the type is known using the new() keyword and omitting the type name. The second list is created the same way, but we passed the argument 10 to its constructor. The third list uses the same approach but explicitly specifies the parameter name, as we could with any standard constructor. Using a named parameter makes the code easier to understand.

The instance of MyClass assigned to the obj variable is created explicitly, but new() is used to create an instance of AnotherClass, which is inferred because the parameter type is known.

The final example demos the use of class initializers. As you may have noticed, the AnotherClass class has an init-only property, which is our next subject.

I can see the target-typed new expressions simplify many codebases. I started using them, and they are a great addition to C# 9.0. Please be careful not to make your code harder to read by abusing target-typed new expressions; only use them when the type is clear, like MyType variable = new().

Init-only properties

Init-only properties are read-only properties that can be initialized using class initializers. Previously, read-only properties could only be initialized in the constructor or with property initializers (such as public int SomeProp { get; } = 2;).

For example, let’s take a class that holds the state of a counter. A read-only property would look like Count:

public class Counter
{
    public int Count { get; }
}

Without a constructor, it is impossible to initialize the Count property, so we can’t initialize an instance like this:

var counter = new Counter { Count = 2 };

That’s the use case that init-only properties enable. We can rewrite the Counter class to make use of that by using the init keyword, like this:

public class Counter
{
    public int Count { get; init; }
}

With that in place we can now use it like this:

var counter = new Counter { Count = 2 };
Console.WriteLine($"Hello, Counter: {counter.Count}!");

Init-only properties enable developers to create immutable properties that are settable using a class initializer. They are also a building block of record classes.

Record classes

A record class uses init-only properties and allows making reference types (classes) immutable. The only way to change a record is to create a new one. Let’s convert the Counter class into a record:

public record Counter
{
    public int Count { get; init; }
}

Yes, it is as simple as replacing the class keyword with the record keyword. Since .NET 6, we can keep the class keyword as well to differentiate (and make consistent) the new record struct, like this:

public record class Counter
{
    public int Count { get; init; }
}

But that’s not all:

  • We can simplify record creation.
  • We can also use the with keyword to simplify “mutating” a record (creating a mutated copy without changing the source).
  • Records support deconstruction, like the tuple types.
  • .NET auto-implements the Equals and GetHashCode methods. Those two methods compare the value of the properties instead of the reference to the object. That means that two different instances with equal values would be equal.
  • .NET auto-overrides the ToString method that outputs a better format, including property values.

All in all, that means we end up with an immutable reference type (class) that behaves like a value type (struct) without the copy allocation cost.

Simplifying the record creation

If we don’t want to use a class initializer when creating instances, we can simplify the code of our records to the following:

public record class Counter(int Count);

Note

That syntax reminds me of TypeScript, where you can define fields in the constructor, and they get implemented automatically without the need to write any plumbing code.

Then, we can create a new instance like with any other class:

var counter = new Counter(2);
Console.WriteLine($"Count: {counter.Count}");

Running that code would output Count: 2 in the console. We can also add methods to the record class:

public record class Counter(int Count)
{
    public bool CanCount() => true;
}

You can do everything with a record that you would do with a class and more. The record class is a class like any other.

The with keyword

The with keyword allows us to create a copy of a record and change only the value of certain properties without altering the others. Let’s take a look at the following code:

var initialDate = DateTime.UtcNow.AddMinutes(-1);
var initialForecast = new Forecast(initialDate, 20, "Sunny");
var currentForecast = initialForecast with { Date = DateTime.UtcNow };
Console.WriteLine(initialForecast);
Console.WriteLine(currentForecast);
public record class Forecast(DateTime Date, int TemperatureC, string Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC /
    0.5556);
}

When we execute that code, we end up with a result similar to this:

Forecast { Date = 9/22/2020 12:04:20 AM, TemperatureC = 20, Summary = Sunny, TemperatureF = 67 }
Forecast { Date = 9/22/2020 12:05:20 AM, TemperatureC = 20, Summary = Sunny, TemperatureF = 67 }

The power of the with keyword allows us to create a copy of the initialForecast record and only change the Date property’s value.

Note

The formatted output is provided by the overloaded ToString method that comes by default with record classes. We have nothing to do to make this happen.

The with keyword is a very compelling addition to the language.

Deconstruction

We can deconstruct record classes like a tuple:

var current = new Forecast(DateTime.UtcNow, 20, "Sunny");
var (date, temperatureC, summary) = current;
Console.WriteLine($"date: {date}");
Console.WriteLine($"temperatureC: {temperatureC}");
Console.WriteLine($"summary: {summary}");
public record class Forecast(DateTime Date, int TemperatureC, string Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

By default, all positional members (defined in the constructor) are deconstructable. In that example, we cannot access the TemperatureF property by using deconstruction because it is not a positional member.

We can create a custom deconstructor by implementing one or more Deconstruct methods that expose out parameters of the properties that we want to be deconstructable, like this:

using System;
var current = new Forecast(DateTime.UtcNow, 20, "Sunny");
var (date, temperatureC, summary, temperatureF) = current;
Console.WriteLine($"date: {date}");
Console.WriteLine($"temperatureC: {temperatureC}");
Console.WriteLine($"summary: {summary}");
Console.WriteLine($"temperatureF: {temperatureF}");
public record Forecast(DateTime Date, int TemperatureC, string Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public void Deconstruct(out DateTime date, out int temperatureC, out string summary, out int temperatureF)
    => (date, temperatureC, summary, temperatureF) = (Date, TemperatureC, Summary, TemperatureF);
}

With that updated sample, we can also access the TemperatureF property’s value when deconstructing the record.

Lastly, by adding Deconstruct methods, we can control the way our record classes get deconstructed.

Equality comparison

As mentioned previously, the default comparison between two records is made by their values and not their memory addresses, so two different instances with the same values are equal. The following code proves this:

var employee1 = new Employee("Johnny", "Mnemonic");
var employee2 = new Employee("Clark", "Kent");
var employee3 = new Employee("Johnny", "Mnemonic");
Console.WriteLine($"Does '{employee1}' equals '{employee2}'? {employee1 == employee2}");
Console.WriteLine($"Does '{employee1}' equals '{employee3}'? {employee1 == employee3}");
Console.WriteLine($"Does '{employee2}' equals '{employee3}'? {employee2 == employee3}");
public record Employee(string FirstName, string LastName);

When running that code, the output is as follows:

Does 'Employee { FirstName = Johnny, LastName = Mnemonic }' equals 'Employee { FirstName = Clark, LastName = Kent }'? False
Does 'Employee { FirstName = Johnny, LastName = Mnemonic }' equals 'Employee { FirstName = Johnny, LastName = Mnemonic }'? True
Does 'Employee { FirstName = Clark, LastName = Kent }' equals 'Employee { FirstName = Johnny, LastName = Mnemonic }'? False

In that example, even if employee1 and employee3 are two different objects, the result is true when we compare them using employee1 == employee3, proving that values were compared, not instances.

Once again, we leveraged the ToString() method of record classes, which is returning a developer-friendly representation of its data. The ToString() method of an object is called implicitly when using string interpolation, like in the preceding code block, hence the complete output.

On the other hand, if you want to know if they are the same instance, you can use the object.ReferenceEquals() method like this:

Console.WriteLine($"Is 'employee1' the same as 'employee3'? {object.ReferenceEquals(employee1, employee3)}");

This will output the following:

Is 'employee1' the same as 'employee3'? False

Conclusion

Record classes are a great new addition that creates immutable types in a few keystrokes. Furthermore, they support deconstruction and implement equality comparison that compares the value of properties, not whether the instances are the same, simplifying our lives in many cases.

Init-only properties can also benefit regular classes if one prefers class initializers to constructors.

What’s new in .NET 6 and C# 10?

.NET 6 and C# 10 have brought many new features. We cannot visit them all but we explore a selection of those features that are leveraged in the book or that I thought were worth mentioning.

In this section, we explore the following C# 10 features:

  • File-scoped namespaces
  • Global using directives
  • Implicit using directives
  • Constant interpolated strings
  • Record struct
  • Minimal hosting model
  • Minimal APIs
  • Nullable reference types (added in C# 8 and enabled by default in .NET 6 templates)

File-scoped namespaces

Declaring a file-scoped namespace reduces the horizontal indentation of our code files by removing the need to declare a block ({}).

We previously wrote:

namespace Vehicles
{
    public interface IVehicleFactory
    {
        // Omitted members
    }
}

We now can write:

namespace Vehicles;
public interface IVehicleFactory
{
    // Omitted members
}

Saving four spaces at the beginning of each line may sound insignificant, but I feel it helps reduce the cognitive load by removing some indentation, and it gives us more screen space for meaningful code.

Global using directives

Before .NET 6, there was always a long list of using directives at the top of each file. Global using directives allow us to define some using directives globally, so those namespaces are automatically imported into every file of the project.

You can add global using directives in any project file, but I recommend centralizing them, so they are not spread around the whole project. There are two places I feel they would fit:

  • In the Program.cs file because that’s the entry point of the program.
  • In a specific file, named meaningfully, like GlobalUsings.cs.

Here is an example that is comprised of three files:

// GlobalUsings.cs
global using GlobalUsingDirectives.SomeCoolNamespace;
// SomeClass.cs
namespace GlobalUsingDirectives.SomeCoolNamespace;
public class SomeClass {  }
// Program.cs
Console.WriteLine(typeof(SomeClass).FullName);

When executing the program, we obtain the following output:

GlobalUsingDirectives.SomeCoolNamespace.SomeClass

Since there is no using directive in the Program.cs file, that proves the global using declared in the GlobalUsings.cs was used, and the whole thing worked as expected.

Implicit using directives

To continue in the way of global using directives, the .NET team gave us a treat: implicit using directives. It is an opt-in feature that is enabled by default in .NET 6 templates by the following property (placed in a PropertyGroup) of your .csproj file:

<ImplicitUsings>enable</ImplicitUsings>

The imported namespaces are stored in an auto-generated [project name].GlobalUsings.g.cs file saved under the obj/Debug/[version] folder. The content varies depending on the project type. As of the time of writing, for console applications, the file contains the following code:

// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

With this enabled, we don’t have to bother with those redundant using directives. We can now write a Hello World program as a one-liner (OK, plus an eight-line csproj file). We can also register our own global using directives in our own files to complement this.

If you don’t like this, you can opt out by deleting the ImplicitUsings property from your project file or by setting its value to disable.

Constant interpolated strings

We do not use this feature in the book, but I felt it was worth mentioning. It happened a few times in my career that I needed this feature.

Note

As a workaround, I used static properties instead, but constants are replaced at build time and are equivalent to hardcoding their value with a lower maintenance overhead (1 constant instead of hardcoded values in multiple places). Using constants is more performant than accessing a property.

Before .NET 6, we could not initialize a constant through interpolation. Now we can as long as all the interpolated values are also string constants. Here is an example:

const string DotNetVersion = "6";
const string BookTitle = $"An Atypical ASP.NET Core {DotNetVersion} Design Patterns Guide";
Console.WriteLine(BookTitle);

That code outputs the following:

An Atypical ASP.NET Core 6 Design Patterns Guide

That code has the same performance as the following:

Console.WriteLine("An Atypical ASP.NET Core 6 Design Patterns Guide");

That’s it, a little more that we can do using C#.

Record struct

This is another feature we do not use in the book but is worth mentioning. These are very similar to record classes but for structure types. As you can see from the following program, the syntax is very similar:

var client1 = new MutableClient("John", "Doe");
client1.Firstname = "Jane";
Console.WriteLine(client1);
var client2 = new ImmutableClient("John", "Doe");
Console.WriteLine(client2);
public record struct MutableClient(string Firstname, string Lastname);
public readonly record struct ImmutableClient(string Firstname, string Lastname);

What is strange compared to record classes is that the positional properties of a record struct are mutable. To make positional properties immutable we must use the readonly record struct keywords instead of record struct.

Executing the code outputs the following result:

MutableClient { Firstname = Jane, Lastname = Doe }
ImmutableClient { Firstname = John, Lastname = Doe }

You should apply the same decision process to record struct versus record class that you’d do to struct versus class. As a rule of thumb, when you are not sure if you should create a struct or a class create a class (the same for record).

Note

Remember that structure types are passed by copy instead of by reference, so a copy occurs every time the struct “moves.”

Minimal hosting model

With the appearance of top-level statements in .NET 5, ASP.NET Core 6 brings a minimal hosting model, which removes the need to create a Program and a Startup class. You can still use the old model, but you don’t need to anymore; you have two options. ASP.NET Core 6 templates leverage this new hosting model by default now.

Concretely, the minimal hosting model is an auto-generated Program class that leverages top-level statements to remove as much plumbing as possible. Here is an example (Program.cs):

var builder = WebApplication.CreateBuilder(args);
// Configure builder.Services here
var app = builder.Build();
// Configure app here
app.Run();

Those three lines of code replace two classes, three methods, the constructor injection of the configuration, and so on. I personally find this more elegant. If your application is larger than a small code sample or you want to test pieces of the registration, nothing stops you from creating extension methods to Add[Feature name] and Use[Feature name] instead of hardcoding everything in the Program.cs file.

As a side note, the auto-generated Program class has an internal visibility modifier, requiring some workaround to test. We explore workarounds in Chapter 2, Automated Testing.

Minimal APIs

With that new hosting model, a few APIs moved to the top of the line, like registering HTTP endpoints. We leverage minimal APIs throughout the book, but the idea is to get rid of as much plumbing as possible and write only what is needed. When building web APIs, we want to create endpoints. Those endpoints don’t always fit well in controllers, and sometimes, even if they do, that seems overkill to do so.

Minimal APIs have a smaller overhead than MVC (fewer features) but offer model binding and dependency injection in a route-to-delegate model. Here is an example (Program.cs) from the GitHub repo associated with Chapter 8, Options and Logging Patterns:

using CommonScenarios;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyOptions>("Options1", builder.Configuration.GetSection("options1"));
builder.Services.Configure<MyOptions>("Options2", builder.Configuration.GetSection("options2"));
builder.Services.Configure<MyDoubleNameOptions>(builder.Configuration.GetSection("myDoubleNameOptions"));
builder.Services.AddTransient<MyNameServiceUsingDoubleNameOptions>();
builder.Services.AddTransient<MyNameServiceUsingNamedOptionsFactory>();
builder.Services.AddTransient<MyNameServiceUsingNamedOptionsMonitor>();
builder.Services.AddTransient<MyNameServiceUsingNamedOptionsSnapshot>();
var app = builder.Build();
app.MapGet("/", (HttpContext context) => new[] {
    new { expecting =  "Options 1", uri = $"https://{context.Request.Host}/options/true" },
    new { expecting =  "Options 2", uri = $"https://{context.Request.Host}/options/false" },
    new { expecting =  "Options 1", uri = $"https://{context.Request.Host}/factory/true" },
    new { expecting =  "Options 2", uri = $"https://{context.Request.Host}/factory/false" },
    new { expecting =  "Options 1", uri = $"https://{context.Request.Host}/monitor/true" },
    new { expecting =  "Options 2", uri = $"https://{context.Request.Host}/monitor/false" },
    new { expecting =  "Options 1", uri = $"https://{context.Request.Host}/snapshot/true" },
    new { expecting =  "Options 2", uri = $"https://{context.Request.Host}/snapshot/false" },
});
app.MapGet("/options/{someCondition}", (bool someCondition, MyNameServiceUsingDoubleNameOptions service)
    => new { name = service.GetName(someCondition) });
app.MapGet("/factory/{someCondition}", (bool someCondition, MyNameServiceUsingNamedOptionsFactory service)
    => new { name = service.GetName(someCondition) });
app.MapGet("/monitor/{someCondition}", (bool someCondition, MyNameServiceUsingNamedOptionsMonitor service)
    => new { name = service.GetName(someCondition) });
app.MapGet("/snapshot/{someCondition}", (bool someCondition, MyNameServiceUsingNamedOptionsSnapshot service)
    => new { name = service.GetName(someCondition) });
app.Run();

Hopefully, it is easy enough to read in the book; essentially, the preceding code leverages the minimal hosting model, registers dependencies with the IoC container, creates the app, then registers five GET endpoints.

The first endpoint creates a JSON menu so you can navigate the project more easily when executing the code. In this endpoint’s delegate, the IoC container injects an HttpContext that is associated with the current request. We can use the context parameter to handle HTTP requests manually. In this case, the code uses the context.Request.Host, but we could use the context.Response.WriteAsync method if we wanted to write to the response stream manually. Here, we return an array of anonymous objects to keep it simple. With minimal APIs, returning an object makes ASP.NET Core serialize it and return it to the client with a 200 OK status code.

The four other endpoints do the same thing but with a different service parameter type. Compared to the first endpoint, these have two parameters:

  • A bool that comes from the route pattern.
  • A service that comes from the IoC container.

Those endpoints leverage the same feature as the first endpoint and return an object that gets serialized automatically and returns it to the client with a 200 OK status code.

Now, if we want to control the output a little more than hoping the framework will do what we want, we can return an implementation of the IResult interface (part of the Microsoft.AspNetCore.Http namespace). Fortunately for us, we do not need to create those implementations and can leverage the static methods of the Results class (same namespace), like this (from the Wishlist example of Chapter 7, Deep Dive into Dependency Injection):

app.MapPost("/", async (IWishList wishList, CreateItem? newItem) =>
{
    if (newItem?.Name == null)
    {
        return Results.BadRequest();
    }
    var item = await wishList.AddOrRefreshAsync(newItem.Name);
    return Results.Created("/", item);
}).Produces(201, typeof(WishListItem));

If you want to define OpenAPI specs, you can also leverage extension methods that are part of the same namespace to describe the endpoints, as we did with the Produces method; explicitly defining this endpoint returns a status code of 201 with a body containing a serialized WishListItem instance.

This new model is a great addition to ASP.NET Core and can be very useful to remove plumbing while remaining optional. If MVC is better for your project, you can call the AddControllers() method and go back to what is best for your project. You can even mix both in the same project.

Nullable reference types

.NET 6 enables nullable reference type checking by default in templates. If you are migrating an existing project, you can enable this feature by adding the following property to your csproj file:

<Nullable>enable</Nullable>

That tells Visual Studio and the .NET compiler to run static code analyzers to detect possible null references. For example, the following code yields a few warnings (highlighted):

var obj = Create(true);
Console.WriteLine($"Hello, {obj.Name}!");
static MyClass? Create(bool shouldYieldANullResult)
{
    return shouldYieldANullResult
        ? default
        : new()
    ;
}
public class MyClass
{
    public string Name { get; set; }
}

The first warning is:

CS8602 Dereference of a possibly null reference.

Which informs us the return value of the Create method can be null (MyClass?). We could fix this by testing if obj is null or with the null-conditional operator (?.), like this:

Console.WriteLine($"Hello, {obj?.Name}!");

The second warning is:

CS8618 Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

This message informs us that the Name property of the MyClass class can be null but was defined as not nullable (string). We can fix this one by marking the property as a nullable string instead, like this:

public class MyClass
{
    public string? Name { get; set; }
}

There are also many attributes available in the System.Diagnostics.CodeAnalysis namespace to deal with null references like NotNull, NotNullWhen, MemberNotNull, and MemberNotNullWhen.

Here is a good resource from Microsoft to help you get started with this, titled Learn techniques to resolve nullable warnings (https://adpg.link/Ljo8).

The .NET team started to update the framework for a few versions before .NET 6, and the default is still just enabled in the template, so if you have a large codebase, you may want to address this iteratively.

Moreover, this feature relies on static analyzers, and the generated IL code is the same as before, so if external consumers call your code or if you call external consumers, runtime errors can still occur. In those cases, it is very important not to put blind confidence in this feature. It is a very good step forward and should help .NET developers write better code, but that’s it.

For example, writing a guard clause to make sure injected values are not null is still useful if the IoC container is not used (or maybe used by someone other than you) or another third-party container is set up in the project. If you rely solely on the .NET IoC container and the analyzers (null-state analysis), no external consumer exists, and you believe that’s safe enough for your project, you can avoid writing guard clauses. If you are writing libraries that consumers could use and have disabled null checks, I suggest writing some. Moreover, guards are pretty cheap to write, so they should not negatively impact the cost of the product you are working on. On the contrary, catching precise errors early can save you time and money.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.201.71