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
Basic Structure of Code Generator
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Execute(SourceGeneratorContext context)
{
// Execute code generation
// Analyse context.Compilation
// Add code, e.g.:
context.AddSource("helloWorldGenerated",
SourceText.From(/* source you want to add */, Encoding.UTF8));
}
public void Initialize(InitializationContext context)
{
// Initialization of code generator
}
}
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<ProjectReference Include="..\SourceGeneratorSamples\SourceGeneratorSamples.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
Sample: INotifyPropertyChanged
[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
public void Initialize(InitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
...
}
class SyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// any field with at least one attribute is a candidate for property generation
if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
&& fieldDeclarationSyntax.AttributeLists.Count > 0)
{
CandidateFields.Add(fieldDeclarationSyntax);
}
}
}
Sample: INotifyPropertyChanged
public void Execute(SourceGeneratorContext context)
{
...
foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
{
SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
{
// Get the symbol being decleared by the field, and keep it if its annotated
IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(
attributeSymbol, SymbolEqualityComparer.Default)))
{
fieldSymbols.Add(fieldSymbol);
}
}
}
// group the fields by class, and generate the source
foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
{
string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
}
}
Sample: INotifyPropertyChanged
private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
{
...
StringBuilder source = new StringBuilder($@"
namespace {namespaceName} {{
public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} {{
");
foreach (IFieldSymbol fieldSymbol in fields)
{
ProcessField(source, fieldSymbol, attributeSymbol);
}
source.Append("}}");
return source.ToString();
}
private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
{
...
source.Append($@"
public {fieldType} {propertyName} {{
get {{ return this.{fieldName}; }}
set {{ this.{fieldName} = value;
this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
}}
}}
");
...
}
Keeping C# Quality High?
Goal: Follow best practices, avoid worst practices
Fostering Quality Culture
-
Be objective
-
Best/worst practices defined by vendors (e.g. Microsoft)
-
Clearly state if something is a subjective opinion
-
Avoid being dogmatic
-
-
Be realistic
-
Every project has a history and technical depts
-
Every project as resource constraints
-
-
Be honest
-
Be polite and appreciative, but be clear about weaknesses
-
Don’t just talk about bad things, call out good practices, too
-
Fostering Quality Culture
-
Software Craftmanship
-
Aim for professionalism, technical excellence
-
Death of the production line and “factory workers” attitude
-
-
Avoid being overly smart
-
Code is more often read than written
-
Code for the readers of your code
-
Write the obvious code first
-
Know your languages and platforms, but avoid exploit being too smart
-
-
Value legacy code and its maintainers
Fostering Quality Culture
-
Quality Ownership
-
Developer is responsible for her/his own code
-
Code reviews process done by experienced programmers (docs)
-
-
Make software quality visible
-
Status
-
Progress
-
See SonarQube later
-
-
Production-first mindset
-
Viewpoint of the customer
-
Goals, Process
-
Find violations of best/good practices for C# code
-
Ideomatic Code (follows conventions of C#)
-
Practices defined by e.g. Microsoft, SonarQube
-
Categories see e.g. Rule sets for NuGet analyzer packages
-
-
Goal: Make code more readable, maintainable, secure, etc.
-
Do
-
…focus on important things
-
…reference “official” guidelines (e.g. rule sets from Microsoft, SonarQube)
-
-
Don’t…
-
…judge based on your (reviewer) personal coding style
-
…spend too much time on less important coding aspects
-
Tools
-
Old, outdated: Visual Studio Code Analysis
-
Does not support .NET Core, .NET Standard
-
-
.NET Compiler Platform ("Roslyn") Analyzers
-
Built-in Analyzers
-
-
New Nullable feature in C# 8
-
Commercial 3rd party code analysis tools
Demo - Really Bad Code 🤯
using System;
using System.Data.SqlClient;
namespace CodeQuality
{
class Program
{
static void Main(string[] args)
{
using var conn = new SqlConnection("...");
conn.Open();
using var cmd = conn.CreateCommand();
Console.WriteLine("Please enter your name");
var name = Console.ReadLine();
cmd.CommandText = $"SELECT ${name} AS NAME";
cmd.ExecuteNonQuery();
}
}
}
Let's analyze this and make it better using Visual Studio editor features
SonarQube
-
SonarQube with SonarLint for Visual Studio
-
Tip: Ready-made Docker image (Docker Hub)
-
Good Azure support (e.g. AAD, Azure SQL DB)
-
SonarQube build tasks for TFS/VSTS (VS Marketplace)
-
Demo: SonarQube in Docker
-
docker run -d --name sonarqube -p 9000:9000 sonarqube
-
-
Demo: SonarQube in Azure and Azure DevOps
What's New in C# 9?
By Rainer Stropek
What's New in C# 9?
- 1,913