What's New
in C#?

Rainer Stropek | @rstropek

Introduction

Rainer Stropek

  • Passionate developer since 25+ years
     
  • Microsoft MVP, Regional Director
     
  • Trainer, Teacher, Mentor
     
  • 💕 community

Code, (nearly) no slides

Use slides to read about details

Factal Tree

Rules of the Game

  • I am allowed to copy code (snippets)
    • Because of limited time
  • Over-engineering is ok to demonstrate C# features
  • Focus on language features, less class library
  • We will not cover every minor detail
  • Learn some things which are not directly related to C# news

record structs

using System;

var p = new Person("Foo", "Bar", 42);
// The following line does not work because records are immutable
// p.LastName = "Baz";
Console.WriteLine(p.FirstName);

var b = new Product("Bike", "Mountainbike", 499m);
Console.WriteLine(b.Name);

// Usual syntax, results in record classes (=reference types)
record Person(string FirstName, string LastName, int Age);

// New syntax "record class"
record class Product(string Category, string Name, decimal Price);

Remember: records

New: Explicitly mention turning record into class

Record Structs are quite similar to Record Classes

IEquatable, ToString, Equals, GetHashCode, you can add methods, etc.

                                   ,
but there are differences, too

using System;

var v1 = new Vector2d(1d, 2d);
v1.X = 3d; // This works because get and set are generated by default
Console.WriteLine(v1.X);
Console.WriteLine(v1); // record structs implement ToString

var v2 = v1 with { X = 4d }; // We can use the with keyword
Console.WriteLine(v2.X);

Span<Vector2d> vectors = stackalloc Vector2d[]
{
    new Vector2d(1d, 2d),
    new Vector2d(3d, 4d),
};

//  New struct record (=value type)
record struct Vector2d(double X, double Y)
{
    public static Vector2d operator +(Vector2d first, Vector2d second) =>
        new Vector2d(first.X + second.X, first.Y + second.Y);
}

New: record struct

using System;
using System.Text;
using System.Text.Json;

var p1 = new Point(1d, 2d);
// p1.X = 3d; // This does not work because readonly records are immutable
Console.WriteLine(p1);

var (x, y) = p1; // Deconstruction works similar to record classes.

var p2 = p1 with { X = 4d }; // We can use the with keyword

// Readonly leads to an immutable struct
// Note that you can use the property: syntax to apply attributes
// or you can apply attributes to manually declared properties.
readonly record struct Point(double X, [property: JsonPropertyName("y")] double Y)
{
    // It is possible to manually declare property.
    [JsonPropertyName("x")]
    public double X { get; init; } = X;

    // Although PrintMembers is private, we can add a custom implementation.
    // C# considers the members as "matching" if signature matches.
    private bool PrintMembers(StringBuilder sb)
    {
        sb.Append($"X/Y = {X}/{Y}");
        return true;
    }
}

More About record struct

record struct Vector3d(double X, double Y, double Z)
{
    // We can turn properties into fields
    // (works for record classes, too)
    public double X = X;
    public double Y = Y;
    public double Z = Z;
}

More About record struct

using System;

var o = new TypeA("FooBar", 42);
Console.WriteLine(o); // Prints "FooBar" because of sealed override

public abstract record BaseRecord(string Name)
{
    // The following line has not been possible before as
    // BaseRecord is not sealed. Now, it is allowed.
    public sealed override string ToString() => Name;
}

public sealed record TypeA(string Name, int Parameter) : BaseRecord(Name);
public sealed record TypeB(string Name, double Parameter) : BaseRecord(Name);

Sealed Override ToString

General enhancement for records, not just record structs

Global Using

Global Using Directive

  • global using System.Text.Json;
    • Works also with using static

  • Motivation
    • Add a single file to your project with using directives
      that you frequently need
    • Makes using lists smaller in other files
    • Similar to Blazor's _Imports.razor file

Global Using

global using System;
global using static System.Console;
global using System.Linq;
global using System.Text;
// Note: no `using System` necessary
var dates = new DateOnly[] {
    new(2021, 1, 1),
    new(2022, 1, 1)
};

// Note: no `using System.Text` necessary
var builder = new StringBuilder();

// Note: no `using System.Linq` necessary
var datesString = dates.Aggregate(
    new StringBuilder(),
    (sb, d) => sb.AppendLine(d.ToString("o")),
    sb => sb.ToString());

// Note: no `Console` necessary
WriteLine(datesString);

Imports.cs

Program.cs

Implicit global using directives

  • To reduce the amount of using directives
  • Enabled by default for .NET >= 6.0
    • Enabled in .csproj 🔗
    • You can disable it if you want 🔗
  • List of namespaces see 🔗

File-scoped Namespaces

File-scoped Namespaces

  • Motivation

    • 99.99% of C# files contain a single namespace directive per file (source)
    • Why do we need the indentation? Code could be simplified.
    • Limitation: Single file-scoped namespace per file (source)
namespace MyApp;
using System;

static class MyUtility
// Will compile into MyApp.MyUtility
{
  public static String Greeting
    => "Hi!";
}
using System;

namespace MyApp
{
  static class MyUtility
  // Will compile into MyApp.MyUtility
  {
    public static String Greeting
      => "Hi!";
  }
}

Definite Assignment Improvements

#nullable enable
using static System.Console;

AnimalFactory? factory = new();

// Everything is fine in this case
if (factory != null && factory.TryGetAnimal(out Animal animal) && animal is Cat c)
{
    WriteLine(c.Purr());
}

class AnimalFactory
{
    public bool TryGetAnimal(out Animal animal)
    {
        animal = new Cat();
        return true;
    }
}

abstract class Animal { }
class Dog : Animal { public string Bark() => "Wuff"; }
class Cat : Animal { public string Purr() => "purrrrr"; }

Definite Assignment Improvements

// The following cases did not work before .NET 6

if (factory?.TryGetAnimal(out Animal animal2) == true && animal2 is Cat c2)
{
    WriteLine(c2.Purr());
}

if (factory?.TryGetAnimal(out Animal animal3) is true && animal3 is Cat c3)
{
    WriteLine(c3.Purr());
}

if ((factory?.TryGetAnimal(out Animal animal4) ?? false) && animal4 is Cat c4)
{
    WriteLine(c4.Purr());
}

if ((factory != null ? factory.TryGetAnimal(out Animal animal5) : false)
    && animal5 is Cat c5)
{
    WriteLine(c5.Purr());
}

Definite Assignment Improvements

Static Abstract Members in Interfaces

Motivation

  • Create abstractions for static members in classes and structs
  • Particularly important for static operators
  • Preview feature 🔗
    • You have to enable it
    • Add <EnablePreviewFeatures>True </EnablePreviewFeatures> in .csproj
using System;

ReadOnlySpan<Vector2d> vectors = stackalloc Vector2d[] { new(1d, 1d), new(2d, 2d), };
Console.WriteLine(AddAll(vectors));

static T AddAll<T>(ReadOnlySpan<T> addables) where T: IAddable<T>
{
    var result = T.Zero;
    foreach (var a in addables) result += a;
    return result;
}

interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

record struct Vector2d(double X, double Y) : IAddable<Vector2d>
{
    public static Vector2d operator +(Vector2d first, Vector2d second)
        => new(first.X + second.X, first.Y + second.Y);
    public static Vector2d Zero => new(0d, 0d);
}

Static Abstract Interface Members

Generic Math in C#

  • Goal: Use operators on generic types (e.g. add two custom vector types)
    • See e.g. INumber 🔗
  • Preview feature 🔗
    • You have to enable it
    • Add <EnablePreviewFeatures>True </EnablePreviewFeatures> in .csproj
    • Reference System.Runtime.Experimental NuGet
public record struct Vector2d<T>(T X, T Y)
    : IAdditionOperators<Vector2d<T>, Vector2d<T>, Vector2d<T>>,
      IAdditionOperators<Vector2d<T>, T, Vector2d<T>>
    where T : INumber<T>
{
    public static Vector2d<T> operator +(Vector2d<T> left, Vector2d<T> right)
        => new(left.X + right.X, left.Y + right.Y);

    public static Vector2d<T> operator +(Vector2d<T> left, T delta)
        => new(left.X + delta, left.Y + delta);
}
var v1 = new Vector2d<int>(1, 1);
var v2 = v1 + new Vector2d<int>(2, 2);
Assert.Equal(new Vector2d<int>(3, 3), v2);
...

...
var v1 = new Vector2d<int>(1, 1);
var v2 = v1 + 2;
Assert.Equal(new Vector2d<int>(3, 3), v2);
...
...
var vs = new[] { new Vector2d<int>(1, 1), new Vector2d<int>(2, 2) };
var sum = new Vector2d<int>(0, 0);
foreach (var v in vs)
{
sum += v;
}

Assert.Equal(new Vector2d<int>(3, 3), sum);
...

λ 
Improvements

using System;

var app = new EndpointConventionBuilder();

// Traditional way of defining a function with an attribute
[HttpGet("/")] int GetAnswer() => 42;
app.MapAction((Func<int>)GetAnswer);

// Now, we can remove the type cast:
app.MapAction(GetAnswer);

// We can even add attributes directly to lambdas:
app.MapAction([HttpGet("/")] () => 42);

Lambda Improvements

using System;

// In the past, we had to use explicit type for lambdas:
Func<int> f = () => 42;

// Lambdas will have a "natural type" that is compatible with var:
var f2 = () => 42;

// We will be able to call lambdas directly:
Console.WriteLine((() => 42)());

Lambda Improvements

Enhancements related to Validations

using System;
using System.Diagnostics.CodeAnalysis;

public class Path
{
    [return: NotNullIfNotNull(nameof(path))]
    public static string? GetFileName(string? path) { /* ... */ }
}

Parameter names in nameof

(moved to C# Next)

using System;

const string s1 = $"abc";
const string s2 = $"{s1}edf";
Console.WriteLine(s2);

DoSomething_Old(42);
DoSomething_VeryOld(42);

[Obsolete($"Use {nameof(DoSomething_New)} instead")]
void DoSomething_Old(int x) { }
void DoSomething_VeryOld(int x)
{
    throw new InvalidOperationException(
      $"{nameof(DoSomething_VeryOld)} is no longer supported");
}
void DoSomething_New(int x) { }

Constant Interpolated Strings

#nullable enable

using System;
using System.Runtime.CompilerServices;

var x = 5;
Verify.Lower(x * 2, Convert.ToInt32(Math.Floor(Math.PI)));

public static class Verify
{
    public static void Lower(int argument, int maxValue,
        [CallerArgumentExpression("argument")] string? argumentExpression = null,
        [CallerArgumentExpression("maxValue")] string? maxValueExpression = null)
    {
        if (argument > maxValue) 
        {
            throw new ArgumentOutOfRangeException(nameof(argument),
                $"{argumentExpression} must be lower or equal {maxValueExpression}");
        }
    }
}

Caller Argument Expressions

// Before
void Insert(string s) {
  if (s is null)
    throw new ArgumentNullException(nameof(s));

  ...
}

// After
void Insert(string s!) {
  ...
}

Simplified Null Validation

(moved to C# Next)

#nullable enable

using System;

Err err;

// Note that we can now mix declaration and tuple deconstruction
(var ret1, err) = GetAnswer();
if (err == null) Console.WriteLine(ret1);

// Go-like error handling anybody?
(var ret2, err) = GetAnswer_Error();
if (err != null) Console.WriteLine(err);

(int?, Err?) GetAnswer() => (42, null);
(int?, Err?) GetAnswer_Error() => (null, new());

class Err { public string Message => "Error"; }

Declarations and Deconstruction

String Interpolation Enhancements

Demo
Time!

What else?

  • Required members (proposal; prototype not started yet)
  • Parameterless constructors with field initializers in structs (proposal)
  • Relax ordering constraints around ref and partial modifiers (proposal)
  • Generic attributes (proposal)
  • Allow deconstruction of default literal (proposal)

C# 10 🤘

Rainer Stropek | @rstropek