how they made our life easier – Part 1

Note: the release of the tenth version of C # has already taken place at the time of the release of the translation of the publication, but the overview of the features will be useful in any case.

With the next C# release coming up, which usually happens in November each year, it’s time to look at the upcoming improvements for C# vNext: C# 10.0. While there aren’t any new mind-blowing designs among them (it’s unrealistic to introduce something like LINQ every year), it’s a number of needed improvements, which is to be expected.

The tenth release of C# can be summed up in one phrase – “getting rid of unnecessary ceremonies”, such as extra curly braces or repetitive code that does not add any value. But such a synopsis does not imply that these changes themselves are not of particular importance. On the contrary, I think that many of these changes are so strong that they will become the norm in C# programming in the future. I suspect that future C# developers won’t even be aware of the old syntax. In other words, some of these improvements are so significant that you will most likely never go back to the old way of writing code unless you need backwards compatibility or code in an earlier version of C#.

Namespace declaration for the entire file (#UseAlways)

To begin with, consider a very simple feature – a namespace declaration for the entire file (file scoped namespace declaration). Previously, when declaring a namespace, the entire contents of that namespace had to be enclosed in curly braces. In C# 10.0, you can declare a namespace before all other declarations (classes, structs, and so on) and not include curly braces after it. As a result, the namespace will automatically include all definitions that appear in the file.

As an example, consider the following code snippet:

namespace EssentialCSharp10;

static class HelloWorld
{
    static void Main() { }
    // ...
}

// Дополнительные объявления пространств имен в том же файле не разрешены
// namespace ScopedNamespaceDemo
// {
// }
// namespace AdditionalFileNamespace;

Here, the CSharp10 namespace is declared first before any other declarations, which is the requirement to use this syntax. In addition, declaring a namespace for the entire file makes it exclusive, i.e. no other namespaces are allowed in the file anymore: neither additional file-wide namespaces, nor traditional namespaces with curly braces.

Although this is a minor change, I feel that I will use this 10.0 feature almost everywhere in the future. Without curly braces, the construction is not only simpler, but also means that I no longer need to indent all other declarations in the namespace. For this reason, I have tagged it with #UseAlways (I will use it all the time) in the title. Also, I think this feature deserves an update to the C# coding guidelines: if you have C# 10.0 or later, use file-wide namespace declarations.

Global Using Directive (#UseAlways)

My #UseAlways recommendation might come as a surprise here, since namespace declarations have changed since C# 1.0, and C# 10.0 includes the second namespace-related change, global namespace directives.

Good programmers refactor well! Why then does C# force us to declare a series of namespaces at the beginning of each file each time? For example, the vast majority of files include a using System directive at the top. Similarly, a unit test project almost always imports the namespace for the target assembly under test and the test environment namespace. So why do we need to write the same using directive over and over for every new file? Wouldn’t it be better to be able to write a single using directive that would act globally for the entire project?

Of course, the answer to this question is “yes”, definitely. For namespaces that you’ve relentlessly specified after using in every file, it’s now possible to provide a new global using directive that will import them throughout the project. The syntax introduces a new global context keyword as a prefix to the standard using directive, as shown in the following XUnit project code snippet:

global using EssentialCSharp10;
global using System;
global using Xunit;

global using static System.Console;

You can place the snippet above anywhere in your code. However, according to the coding convention, it is better to do this in GlobalUsings.cs or Usings.cs. After you write global using directives once, you can use the result in all files of your project:

public class SampleUnitTest
{

    [Fact]
    public void Test()
    {
        // Прописывать using System не нужно.
        DateTime dateTime = DateTime.Now;

        // Прописывать using Xunit не нужно.
        Assert.True(dateTime <= DateTime.Now);
        
        WriteLine("...");
    }
}

Note that the using directives include support for the use of static, whereby you can have a statement WriteLine() without the “System.Console” qualifier. Global aliases using directive syntax are also supported.

In addition to writing global using statements explicitly in C#, you can also declare them in MSBuild (as of version 6.0.100-rc.1). for example Using element of your CSPROJ file (i.e. <Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />) creates a file ImpklicitNamespaceImports.cs, which includes the corresponding global namespace declaration. Also, adding a static attribute (for example, Static="true") or an alias attribute such as Alias=" UnitTesting", creates the corresponding static or alias directives. Also, some frameworks include implicit global directives. The relevant list can be found here. However, if you prefer that such global namespaces not be generated by default, you can disable them with the element ImplicitUsingsset to disabled or false. Below is an example PropertyGroup from .csproj file:

<PropertyGroup>
    <ImplicitUsings>disable</ImplicitUsings>
    <Using>EssentialCSharp10</Using>
    <Using>System</Using>
    <Using>Xunit</Using>
    <Using Static="true">System.Console</Using>
</PropertyGroup>

It should be noted that this does not yet work in Visual Studio 2022 Preview 3.1.

You don’t need to make all your using directives global, otherwise you’re very likely to run into ambiguities in the short form of type names. However, I’m more than sure that every large project will have at least a few global declarations (such as the default declarations for some framework), hence the tag #UseAlways.

Interpolated constant strings (#UsedFrequently)

One feature that has no doubt annoyed you so far is the lack of a method for declaring an interpolated constant string, even if the value is entirely determined from other constants or compile-time-determined values. C# 10 finally solved this problem. Here are some examples:

const string author = "Mother Theresa";
const string dontWorry = "Never worry about numbers.";
const string instead = "Help one person at a time and always " + 
    "start with the person nearest you.";
const string quote = $"{ dontWorry } { instead } - { author }";

One of the times I’m particularly grateful for constant interpolation is when using nameof inside attributes, as shown in the following code snippet:

[Obsolete($"Use {nameof(Thing2)} instead.")]
class Thing1 { }
class Thing2 { }

Prior to C# 10.0, the inability to use the nameof operator within a constant string literal was no doubt confusing.

lambda improvements

C# 10.0 includes three improvements to lamb syntax, both for expressions and statements.

Attributes

With the many new parameter attributes introduced in C# 8 with nullable references, the lack of support for attributes in lambda expressions has become even more pronounced. Luckily, C# 10 finally brought us support for lambda expression attributes, including return type attributes:

Func<string?, string[]?>? Func = [return: NotNullIfNotNull("cityState")]
    static (string? cityState) => cityState?.Split(", ");

Note that in order to use attributes in a lambda expression, you must enclose the parameter list in parentheses. _ = [return: NotNullIfNotNull("cityState")] cityState => cityState?.Split(", ") not allowed.

Explicit return type

If you are used to using an implicit type declaration with var, then you know firsthand that the compiler often cannot determine the signature of a method. Consider, for example, a method that returns null if it fails to convert text to nullable int:

var func = (string? text) => int.TryParse(text, out number)?number:null;

The problem here is that int?and object will be a valid return. And it’s not obvious what to use. Although the result can be cast, the syntax for casting a large expression is cumbersome. The preferred alternative, available in C# 10.0, is to allow the return type to be declared within lambda syntax:

Func<string?, int?> func = int? (string? text) => 
    int.TryParse(text, out int number)?number:null;

An added bonus for those who don’t use it that often varis that adding a return type declaration allows you to perform quick conversion actions var to an explicit lambda type, as shown in the code snippet above: Func<string?, int?>.

Note that lambda expressions declared with the syntax delegate { }not supported: Func<int> func = delegate int { return 42; } won’t compile.

Natural function types

Finally, you can infer the natural delegate type for lambdas and expressions. Essentially, the compiler will attempt to determine the “best fit” signature of the lambda or expression, and thus allow the programmer to avoid overspecifying types when the compiler itself can infer the type.

Caller attribute (#UsedRarely)

This feature has three different usage profiles. If you write perfect code that never needs tweaking or revision, the next feature will probably be useless to you. But if you do use debug classes, logging classes, or unit test assertions, this feature can be a real boon even if you don’t know it exists. Essentially, you get the benefits of this feature without having to make any adjustments to your code. If you are a library vendor and provide validation or assertion logic, it is imperative that you use this feature wherever possible. This will greatly improve the functionality of your API.

Working with caller attribute methods

Imagine a function that validates a string parameter to see if it’s empty or null. You can use such a function on a property like this:

using static EssentialCSharp10.Tests.Verify;
class Person
{
    public string Name
    {
        get => _Name ?? "";
        set => _Name = AssertNotNullOrEmpty(value);
    }
    private string? _Name;
}

There is no visible C# 10.0 feature in this code snippet. Name is a non-nullable property, with an assert method that throws an exception if the value is null or empty. So what is the feature?

The difference shows up at run time. In this case, when the value is null or empty, method AssertNotNullOrEmpty() throws an exception ArgumentNullwhose message includes an argument expression – “value“. And, if the method call was AssertNotNullOrEmpty("${firstName}{lastName}")then ArgumentNull will include the exact text: "${firstName}{lastName}"because it was an argument expression that was specified when the method was called.

Logging is another area where this feature can be very useful. Instead of calling Logger.LogExpression($"Math.Sqrt(number) == { Math.Sqrt(number) }" you could call Logger.LogExpression(Math.Sqrt(number)) and give the method Log() include both value and expression in the output message.

Implementing Caller Attribute Methods

One of the major advantages caller attributes is an advanced feature (which you don’t have to worry about) without making any changes to the source code. But you need to understand how to declare and use this feature when implementing assert, logging or debug methods. Here is the code demonstrating AssertNotNullOrEmpty():

public static string AssertNotNullOrEmpty(
    string? argument,
    [CallerArgumentExpression("argument")]
        string argumentExpression = null!)
{
    if (string.IsNullOrEmpty(argument))
    {
        throw new ArgumentException(
            "Argument cannot be null or empty.",
            argumentExpression);
    }
    return argument;
}

The first thing to look at is the attribute CallerArgumentExpressiondecorating parameter argumentsExpression. With the addition of this attribute, the C# compiler inserts the expression given as an argument into argumentsExpression. In other words, although the calling expression was written as _Name = AssertNotNullOrEmpty(value)the C# compiler converts the call to _Name = AssertNotNullOrEmpty(value, "value"). As a result, the method AssertNotNullOrEmpty() now has a computed value for the argument expression (in this case, the value “value” and the expression itself). Thus, upon occurrence ArgumentException the message can not only indicate what went wrong, “Argument cannot be null or empty”, but also provide the text of the expression “value“.

note that CallerArgumentExpression includes a string parameter specifying the parameter whose expression will be CallerArgumentExpression in the implementation method. In this case, since we have specified “argument”, into the meaning argumentsExpression parameter expression will be substituted “argument“.

As a result, the use CallerArgumentExpression not limited to just one parameter. You could, for example, write AssertAreEqual(expected,, [CallerArgumentExpression("expected")] string expectedExpression = null!, [CallerArgumentExpression("actual") ] string actualExpression = null!) and as a result, throw exceptions that show the expressions themselves, and not just the final results.

When implementing CallerArgumentExpression the following recommendations should be taken into account:

  • Declare a parameter CallerArgumentExpression optional (using "=null!") so that the method call does not require the caller to explicitly define the expression. In addition, it allows you to add a feature to existing APIs without changing the caller code.

  • Consider declaring a parameter CallerArgumentExpression both non-nullable and value assignment null using the null-forgiving operator (!). This allows the compiler to specify a default value and implies that it must not be null if an explicit value is given.

Sadly, in parenthesis, as of C# 10.0, you can’t use nameof to identify the parameter. For example, CallerArgumentExpression(nameof(argument)) will not work. This is because the argument parameter is not in scope at the time of the attribute declaration. However, such support is under consideration after C# 10.0 (see Support for Method Parameter Names in nameof(): https://github.com/dotnet/csharplang/issues/373).


We invite everyone to an open lesson “Generic drugs, their implementation and limitations.” In the lesson, we will consider generic types and methods, the reasons for their appearance, their use; Let’s discuss the limitations of generics and options for inheriting generic types. Registration link.

Similar Posts

Leave a Reply