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
- Analyse code at compile time and generate code
What's New in C# 9?
By Rainer Stropek
What's New in C# 9?
- 2,139