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:
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 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 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.
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.
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 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:
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
.
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 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.
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 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.
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.
In this section, we explore the following C# 9 features:
new
expressionsWe 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.
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 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 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.
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:
with
keyword to simplify “mutating” a record (creating a mutated copy without changing the source).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.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.
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 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.
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.
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
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.
.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:
using
directivesusing
directivesDeclaring 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.
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:
Program.cs
file because that’s the entry point of the program.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.
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
.
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#.
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.”
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.
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:
bool
that comes from the route pattern.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.
.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.
3.145.201.71