What's New in C# 9?

Top-Level Statements

Goals: Less boilerplate code for simple use cases,
make C# simpler to approach

Top-Level Statements

System.Console.WriteLine("Hello World!");
using System;
using System.Threading.Tasks;

await Task.Delay(100);
Console.WriteLine("Hello World!");
using System;
using System.Threading.Tasks;

if (args.Length == 0)
{
	Console.Error.WriteLine("Arguments missing");
    return;
}

Console.WriteLine($"Received {args[0]}");

Use top-level async/await

Access command-line arguments

Tip: Take a look at FeatherHttp

Demo
Time!

Attributes on Local Functions

Goal: Enable use of local functions in more scenarios

Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.ConfigureServices(services =>
        {
            services
              .AddAuthentication("MyScheme")
              .AddScheme<DummyAuthenticationOptions, DummyAuthenticationHandler>("MyScheme", options => { });
            services.AddAuthorization();
        })
        .Configure(app =>
        {
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                static async Task SayHello(HttpContext context) => await context.Response.WriteAsync("Public view");
                endpoints.MapGet("/", SayHello);

                [Authorize(AuthenticationSchemes = "MyScheme", Roles = "Admin")]
                static async Task AdminsOnly(HttpContext context) => await context.Response.WriteAsync("Admin view");
                endpoints.MapGet("/secret", AdminsOnly);
            });
        });
    })
    .Build().Run();

Attributes on Local Functions

Demo
Time!

Target-typed new

Goal: Let the compiler infer the type in more cases

Quick Recap:
Default Value Expressions

// `default` operator
Console.WriteLine(default(int));  // output: 0
Console.WriteLine(default(object) is null);  // output: True

// Since C# 7: `default` literal
// Initialization
int x = default;

// Default arguments
static T GetElementAtIndex<T>(int index = default) { ... }

// Argument value
static TOutput Map<TInput, TOutput>(TInput objectToTransform, Func<TInput, TOutput> transformFunction, bool log) { ... }
Map("Hi", s => s.ToUpper(), default);

// Return statement
static int Div(int x, int y) => y == 0 ? default : x / y;

Constructing Classes and Structs

using System;

struct S { }

class CtorWithParameters
{
    public CtorWithParameters(int x) { /* ... */ }
}

class C
{
    public static void Main()
    {
        int x1 = new();
        var x2 = (int)new();
        (int x, int y) point = new();

        C myClass = new();
        var myClass2 = (C)new();
        myClass = null;
        myClass ??= new();
        CtorWithParameters myClass4 = new(42);
        
        S myStruct = new();
    }
}

Better Type Inference

using System;
using System.Collections.Generic;

class Person
{
    public string FirstName { get; }
    public string LastName { get; }
    public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName);
}

class C
{
    public static void Main()
    {
        List<Person> people = new()
        {
            new("Foo", "Bar"),
            new("John", "Doe")
        };
        var list = new[] { new C(), new() }; // list will be C[]
        var list2 = new[] { new Person("Foo", "Bar"), new("John", "Doe") };
    }
}

Generics

class CreationOptions { /* ... */}

class C
{
  static T Factory<T>() where T : new() => new();
  static T Factory<T>(CreationOptions options) where T : new()
  {
    // Check options
    return new();
  }

  public static void Main()
  {
    var program = Factory<C>(new());
  }
}

Better Type Inference & Generics

using System.Collections.Generic;

class Person
{
  public string FirstName { get; }
  public string LastName { get; }
  public Person(string firstName, string lastName) 
    => (FirstName, LastName) = (firstName, lastName);
}

class Manager : Person
{
  public bool IsSenior { get; }
  public Manager(string firstName, string lastName, bool isSenior)
      : base(firstName, lastName)
      => IsSenior = isSenior;
}

class Employee : Person
{
  public bool IsIntern { get; }
  public Employee(string firstName, string lastName, bool isIntern)
      : base(firstName, lastName)
      => IsIntern = isIntern;
}
class Program
{
  static void Main()
  {
    Dictionary<Manager, List<Employee>> orgChart = new()
    {
      {
        new("Foo", "Bar", true),
        new()
        {
          new("John", "Doe", true),
          new("Jane", "Smith", false)
        }
      }
    };
  }
}

In Enumerator Blocks

using System;
using System.Collections.Generic;

class Person
{
  public string FirstName { get; }
  public string LastName { get; }
  public Person(string firstName, string lastName)
    => (FirstName, LastName) = (firstName, lastName);
}

class Program
{
  static IEnumerable<Person> GetPeople()
  {
    for (var i = 0; i < 10; i++)
    {
      yield return new($"Foo{i++}", "Bar");
    }
  }

  static void Main()
  {
    foreach (var person in GetPeople())
    {
      Console.WriteLine(person);
    }
  }
}

Switch Expressions

enum HeroType
{
  NuclearAccident, FailedScienceExperiment,
  Alien, Mutant,
  Other
};

enum HeroTypeCategory
{
  Accident, SuperPowersFromBirth, Other
}

class Hero
{
  public Hero(HeroTypeCategory category) { }
}

class Program
{
  static void Main()
  {
    var ht = HeroType.FailedScienceExperiment;
    Hero h = ht switch
    {
      HeroType.Alien or HeroType.Mutant => new(HeroTypeCategory.SuperPowersFromBirth),
      HeroType.FailedScienceExperiment or HeroType.NuclearAccident => new(HeroTypeCategory.Accident),
      _ => new(HeroTypeCategory.Other)
    };
  }
}

Lambda Discard Parameters

Goal: No longer invent dummy parameter names in lambda expressions

Lambda Discard Parameters

MathOperation<int> add = (a, b) => a + b;

// Previously:
MathOperation<int> dummyOld = (_, __) => 42;

// Now:
MathOperation<int> dummyNew = (_, _) => 42;

delegate T MathOperation<T>(T a, T b);

Enhanced Pattern Matching

Goal: Make pattern matching even more useful

Relational Patterns

using System;

var age = 84;

if (age is >= 65)
{
    Console.WriteLine("Is senior");
}

var ageCategory = age switch
{
    < 13 => "child",
    < 18 => "teenager",
    < 65 => "adult",
    _ => "senior"
};

Console.WriteLine(ageCategory);

Pattern Combinators

enum HeroType
{
  NuclearAccident, FailedScienceExperiment,
  Alien, Mutant,
  Other
};

enum HeroTypeCategory
{
  Accident, SuperPowersFromBirth, Other
}

class Hero
{
  public Hero(HeroTypeCategory category) { }
}

class Program
{
  static void Main()
  {
    var ht = HeroType.FailedScienceExperiment;
    Hero h;
    if (ht is HeroType.Alien or HeroType.Mutant)
    {
      h = new(HeroTypeCategory.SuperPowersFromBirth);
    }
  }
}

Relational + Combinators

using System;

class Program
{
  static void Main()
  {
    var letter = 'x';
    bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
    Console.WriteLine(IsLetter(letter));

    var number = 42;
    bool IsInRange(int n) => n is > 40 and < 50;
    Console.WriteLine(IsInRange(number));

    object someObject = number;
    if (someObject is int and not < 42) Console.WriteLine("High Number");
  }
}

Records

Goal: Make data-only classes and structs simpler

Simple Record Class - Generated Code

using System;

class Program
{
    public static void Main()
    {
        var p = new Point(42, 84);
        Console.WriteLine($"{p.X}/{p.Y}");
    }
}

record Point(int X, int Y);

Record Struct with Equality Comparer

using System;

class Program
{
    public static void Main()
    {
        var p1 = new Point(42, 84);
        var p2 = new Point(42, 84);
        Console.WriteLine(p1.Equals(p2));
    }
}

record Point(int X, int Y);

With Expression

using System;

var c = new C(0, 1);
Console.WriteLine(c); // prints: 0 1
c = c with { X = 5 };
Console.WriteLine(c); // prints: 5 1
c = c with { Y = 2 };
Console.WriteLine(c); // prints: 5 2

record C(int X, int Y)
{
    public override string ToString() => X + " " + Y;
}

What Else?

What didn't we mention here (because too early, only for a small niche, etc.)?

  • Relax ordering constraints around ref and partial (details)
  • Suppress emitting of locals init flag (details)
    • ​JIT compiler injects code initializing all local vars before starting the method
  • ​Native-Sized Number Types (details)
  • Function pointers (details)
  • Static Lambda Expressions (details)
    • ​Throw error when unintentionally capturing any local state
  • Target-typed conditional expression (details)
    • ​Enhance type handling in conditional expressions like c ? e1 : e2
  • Covariant Return Types (details)
  • Extension GetEnumerator Recognition in foreach (details)
  • Module initializers (details)
  • Extending Partial Methods (details)
  • Top-level Statements (details)

Q&A

Thank you for attending

C# Source Generators

Goal: Generate Code in VS and during compilation

What's a Code Generator?

  • Runs during compilation
  • Inspects your program (using Roslyn)
  • Produces additional files that are compiled together with your app
  • .NET Standard 2.0 assembly loaded with analyzers
  • Current Status: First Preview, ​not ready for production yet

Why Code Generators?

  • Currently:
    • Runtime reflection, e.g. ASP.NET Core DI (impacts performance)
    • Change IL after compilation ("IL weaving")
    • MSBuild tasks
  • In the future:
    • Analyse code at compile time and generate code
      • Similar to Google's Wire DI framework (Go)
    • Less reflection leads to...
      • ...better performance
      • ...smaller apps because AoT compiler (linker) can remove unused parts of your code