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

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

  • 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

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