Goals: Less boilerplate code for simple use cases,
make C# simpler to approach
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
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();
Goal: Let the compiler infer the type in more cases
// `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;
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();
}
}
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") };
}
}
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());
}
}
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)
}
}
};
}
}
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);
}
}
}
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)
};
}
}
Goal: No longer invent dummy parameter names in lambda expressions
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);
Goal: Make pattern matching even more useful
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);
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);
}
}
}
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");
}
}
Goal: Make data-only classes and structs simpler
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);
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);
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 didn't we mention here (because too early, only for a small niche, etc.)?
Goal: Generate Code in VS and during compilation
[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>
[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);
}
}
}
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));
}
}
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})));
}}
}}
");
...
}
Goal: Follow best practices, avoid worst practices
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
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
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
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
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
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 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