13. March 2025 | Tech Insights by Pavol Mráz | CTO: My favorite C# features in real world use cases
Introduction
I have read a ton of articles about someone’s favorite C# language features on the Internet (especially on medium.com) and some of them are really good and inspiring. I have decided that I might add two cents to the discussion, and this is my take on the subject. It is a little bit different than the others, because I present real-world usage patterns, stemmed from our large code base (980+ KLOC) that is undergoing continuous development for more than nine years now (January, 2025). The code base is kind of a modular monolith comprised of several business applications, APIs, as well as some reusable functionality in the form of NuGet packages.
In the discussion and code examples below, I have renamed some of the code artifacts, but the actual usage patterns remained intact. I have not tried to restate what the official documentation says, instead, I am providing links to its relevant sections. I emphasize here that these are my subjective opinions, and not all my colleagues agree with me 100%.
Field keyword
The contextual field keyword can be used in property accessors to access the compiler-generated backing field of a property. We have been using it mostly to add validation to property setters implemented originally as auto-properties. For example, we had this code before introducing the field keyword:
class Person
{
public string Name { get; set; }
}
After gradually introducing nullability to our code base, we were able to add property setter validation easily using the field keyword like this:
class Person
{
public string Name { get; set { field = value ?? throw new ArgumentNullException(nameof(value)); } } = "";
}
Please note that the official documentation says that:
“The field keyword is a preview feature in C# 13. You must be using .NET 9 and set your <LangVersion> element to preview in your project file in order to use the field contextual keyword.”
However, we have been using it in projects targeting netstandard2.0 without problems just with the <LangVersion>preview</LangVersion> property set in .csproj (or Directory.Build.props file). This is understandable, because the field keyword is just a syntactic sugar allowing us to access the backing field without needing to know the actual compiler-generated mangled name as this gist from sharplab.io illustrates.
Bonus chatter: First time I have used this feature, I wrote the code this way (note the property name in the setter):
class Person
{
public string Name { get; set { Name = value ?? throw new ArgumentNullException(nameof(value)); } } = "";
}
Neither the compiler nor any of the analyzers warned me, so I realized the mistake only after running the program and getting an StackOverflowException upon the very first Name property assignment. Now it is obvious that referencing the Name property inside the property setter method creates infinite recursion and I honestly do not know, what led me to write it that way.
Pattern matching and switch expressions
This feature was first introduced in C# version 7.0 but since then, it has been gradually enhanced in almost all subsequent releases, including switch expressions in C# version 8.0. We have been using it mostly when working with enums, because the compiler ensures that we always cover all possible enum values (the switch expression is exhaustive as they call it). All but one of the usage patterns I have found use constant pattern matching mapping from enum to some other value. For example, mapping from MqttNetLogLevel (from the excellent MQTTnet library) to Microsoft’s standard LogLevel:
using Microsoft.Extensions.Logging;
using MQTTnet.Diagnostics;
...
LogLevel netLogLevel = logLevel switch
{
MqttNetLogLevel.Error => LogLevel.Error,
MqttNetLogLevel.Warning => LogLevel.Warning,
MqttNetLogLevel.Info => LogLevel.Information,
MqttNetLogLevel.Verbose => LogLevel.Trace,
_ => LogLevel.Debug
};
The exhaustive mapping can be illustrated by omitting the last line with the discard expression “_ => LogLevel.Debug”. The then complains with a clear explanation: error CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern ‘(MQTTnet.Diagnostics.MqttNetLogLevel)4’ is not covered.
The only other switch expression not using the constant pattern over enum, but based on a relational pattern matching expression I found in the code base was in a MAUI Android application. The application uses GPS for location tracking and maps the horizontal location accuracy from meters to application-defined enumeration like this:
public enum GpsLocationQuality
{
Unknown = 0,
Poor = 1,
Good = 2,
Excellent = 3
}
...
Android.Locations.Location location = <get location from ILocationListener>
GpsLocationQuality quality = location.Accuracy switch
{
<= 0 => GpsLocationQuality.Unknown,
< 5 => GpsLocationQuality.Excellent,
<= 15 => GpsLocationQuality.Good,
_ => GpsLocationQuality.Poor
};
Please note that when we omit the last discard expression, the compiler still enforces the exhaustive mapping by giving us the following error:
error CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '15.000001F' is not covered
Raw string literals
The Raw string literals feature was introduced in C# 11 and I did find just one use case in our code base that I have actually implemented myself 😊. It was as a portion of integration test suite for legacy SOAP asmx API that was migrated to ASP.NET Core (using the excellent SoapCore package). I have used a testing approach similar to what Michael feathers calls “characterization testing” in his excellent book “Working Effectively with Legacy Code”:
- I have captured the response XML payloads returned by invoking the legacy SOAP endpoints over a seed database.
- I have written tests that invoked the new ASP.NET Core endpoints (over the same seed database) and compared the XML payloads with the captured legacy ones.
For the payload comparison, I have used the XMLUnit.Core package and the legacy response I have put directly into the test code using raw string literals like this:
[Fact]
public async Task Verify_SomeService_response()
{
string expectedXmlResponse =
"""
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<endOfDayResponse
xmlns="http://example.com/common/service/types/SomeService/1.0">
<manifestPDF
xmlns="">some value
</manifestPDF>
</endOfDayResponse>
</soap:Body>
</soap:Envelope>
""";
string soapCoreXmlResponse = await InvokeSoapCoreServiceAsync(<hardcoded parameters>);
var diff = DiffBuilder
.Compare(expectedXmlResponse)
.WithTest(soapCoreXmlResponse)
.IgnoreWhitespace()
.Build();
Assert.False(diff.HasDifferences());
By using raw string literals, I was able to copy the legacy SOAP payloads directly from Postman response bodies and paste them into the test methods verbatim.
Records, init properties, deconstruction
The records feature introduced in C# 9.0 we have been using almost exclusively as a convenient way to define data transfer objects. The automatic read-only property declaration when using records with primary constructors makes them very convenient and terse (and elegant IMHO):
public record class GrantAccessTokenRequestModel(
string UserName,
string Password,
string ClientId);
The declaration creates an immutable record type and with the help of deconstruction using the with keyword, we can create clones at will:
var requestModel = new GrantAccessTokenRequestModel(
UserName: form[RequestFormKeys.UserName],
Password: form[RequestFormKeys.Password],
ClientId: form[RequestFormKeys.ClientId]);
var clone = requestModel with { Password = "********" };
In order to customize serialization of these record instances, we tend to do that by appropriate application of the JsonPropertyNameAttribute, along with init and required C# attributes, for example:
public record class GrantAccessTokenResponseModel
{
[JsonPropertyName("access_token")]
public required string AccessToken { get; init; }
[JsonPropertyName("refresh_token")]
public required string RefreshToken { get; init; }
[JsonPropertyName("expires_in")]
public required int ExpiresInMinutes { get; init; }
[JsonPropertyName("token_type")]
public string TokenType { get; } = "Bearer";
}
The above declaration ensures proper serialization to JSON using the specified JSON property names. The declaration also states that the AccessToken, RefreshToken and ExpiresInMinutes properties must be initialized upon construction, for example:
var response = new GrantAccessTokenResponseModel()
{
AccessToken = accessTokenString,
ExpiresInMinutes = (int)(expirationTimeUtc - DateTime.UtcNow).TotalMinutes,
RefreshToken = refreshToken
};
var response = new GrantAccessTokenResponseModel()
{
AccessToken = accessTokenString,
ExpiresInMinutes = (int)(expirationTimeUtc - DateTime.UtcNow).TotalMinutes,
RefreshToken = refreshToken
};
Please note that we use the record class declaration with the class keyword to make it explicit that we are declaring a reference type (not the record struct value type).
Author: Pavol Mráz | Chief Technology Officer:
Throughout his professional career, he has contributed to the design, development, and maintenance of customer information systems for both private and public sector companies, as well as corporate tools and methodologies aimed at streamlining software development.
Examples include the service software for the voting system of the Slovak Parliament, the system for tracking technical changes for Volkswagen Slovakia, and the corporate authorization framework Constable.
Contact
ForesServices, s. r. o.
Prievozská 14
821 09 Bratislava
Tel.: 421 911 114 957
Email: info@fores.group
Web: www.fores.group