.NET 5.0 and C# 9.0 New Features

Content:

  1. .NET Framework vs .NET Core vs .NET
  2. Version numbers
  3. New features
    1. General
    2. Init only properties
    3. Relational operator patterns
    4. Target typed new
    5. Script style
    6. Records
    7. nint/nuint

.NET Framework vs .NET Core vs .NET:

What they are:

.NET Framework versions 1.0 to 4.8 are Microsofts .NET implementation. It was succeeded by .NET 5.0 in 2020. It was for Windows only.

.NET Core versions 1.0 to 3.1 are the open source implementation of .NET supported by Microsoft. It was renamed to just .NET in 2020. It was for Windows, Linux and macOS.

.NET version 5.0 and later are the common open source and Microsoft implementation of .NET from 2020 an onwards. It is based on .NET Core 3.1 - not on .NET Framework 4.8. It is for Windows, Linux and macOS.

What they contain:

.NET Framework and .NET Core does not consist of exactly the same components.

.NET FX vs .NET Core

I have not worked enough with .NET 5.0 to have verified everything below - it is just based on what I have read on the internet, so there may be some inaccuracies.

Core (portable):

  1. basic stuff
  2. Numerics
  3. Globalization & Text
  4. IO
  5. Net
  6. Collections
  7. XML
  8. ADO.NET
  9. LINQ
  10. Threading
  11. Reflection
  12. WCF Client
  13. ASP.NET MVC
  14. Compiler services

Core (Windows only):

  1. Win Forms
  2. WPF

Core (new):

  1. DI
  2. System.Text.Json

Extension package - not part of .NET Core but available via NuGet:

  1. ODBC
  2. OLE DB
  3. Microsoft.Win32.Registry
  4. System.Configuration.ConfigurationManager
  5. System.Runtime.Caching

FX only:

  1. ASP.NET Web Forms
  2. WCF Server
  3. Remoting
  4. WWF
  5. Enterprise Services

BinaryFormatter is highly deprecated in .NET Core.

How they are different:

.NET Framework is integrated in Windows limiting the ability to have different versions side by side. .NET Core is just stuff in a directory tree making it much easier to have different versions side by side. .NET Core does not have GAC either.

When using Visual Studio the difference between .NET Framework and .NET Core is almost invisible. You pick the correct template when you create the solution and then Visual Studio handles the rest. But using command line tools are very different.

In .NET Framework one could build with the command line compiler:

csc something.cs

That is practicall impossible with .NET Core - instead one need to use the provided tools and create a project.

Create console project in current directory:

dotnet new console

Add NuGet package:

dotnet add package Xxx.Yyy.Zzz

Build project:

dotnet build

Run project:

dotnet run

Generate deployable bundle including an EXE file:

dotnet publish

.NET Framework used XML config files (app.config -> xxx.exe.config for console applications, web.config for ASP.NET applications).

app.exe.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
        <add key="Key#1" value="Value #1" />
        <add key="Key#2" value="Value #2" />
        <add key="Key#3" value="Value #3" />
     </appSettings>
</configuration>

app.cs:

using System;
using System.Configuration;

public class App
{
    public static void Main(string[] args) 
    {
        Console.WriteLine(ConfigurationManager.AppSettings["Key#1"]);
        Console.WriteLine(ConfigurationManager.AppSettings["Key#2"]);
        Console.WriteLine(ConfigurationManager.AppSettings["Key#3"]);
    }
}

.NET Core is much more flexible.

The standard and recommended is JSON config files (appsetting.json and appsettings.xxxx.json).

appsettings.json:

{
    "AppSettings" : {
        "Key#1" : "Value #1",
        "Key#2" : "Value #2",
        "Key#3" : "Value #3"
    }
}

app.cs:

using System;

// dotnet add package Microsoft.Extensions.Configuration
// dotnet add package Microsoft.Extensions.Configuration.Json
using Microsoft.Extensions.Configuration;

public class App
{
    public static void Main(string[] args)
    {
        IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true);
        IConfiguration config = builder.Build();
        Console.WriteLine(config["AppSettings:Key#1"]);
        Console.WriteLine(config["AppSettings:Key#2"]);
        Console.WriteLine(config["AppSettings:Key#3"]);
    }
}

But it is also possible to use the same XML file as in .NET Framework.

app.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
        <add key="Key#1" value="Value #1" />
        <add key="Key#2" value="Value #2" />
        <add key="Key#3" value="Value #3" />
     </appSettings>
</configuration>

app.cs:

using System;
// dotnet add package System.Configuration.ConfigurationManager
using System.Configuration;

public class App
{
    public static void Main(string[] args) 
    {
        Console.WriteLine(ConfigurationManager.AppSettings["Key#1"]);
        Console.WriteLine(ConfigurationManager.AppSettings["Key#2"]);
        Console.WriteLine(ConfigurationManager.AppSettings["Key#3"]);
    }
}

There are lots of other new stuff in .NET Core.

For a description of the new .NET Core System.Text.Json namespace see code here.

For a description of .NET Core DI see Core/MS Extension code here.

There are lots of other changes around the framework. Just as an example see the notes in Web Service - Standalone article.

Version numbers:

Version numbers are a bit complicated.

.NET Framework:

.NET framework .NET runtime C# language/compiler Visual Studio IDE Release date
1.0 1.0 1.0 2002 February 13th 2002
1.1 1.1 1.2 2003 April 24th 2003
2.0 2.0 2.0 2005 November 7th 2005
3.5 2.0 3.0 2008 November 19th 2007
4.0 4.0 4.0 2010 April 12th 2010
4.5 4.0 5.0 2012 August 15th 2012
4.5.1 4.0 5.0 2013 October 17th 2013
4.6 4.0 6.0 2015 July 20th 2015
4.6.2 4.0 7.0 2017 March 7th 2017 (framework August 2nd 2016)
4.6.2 4.0 7.1 2017 update 3 August 14th 2017
4.7.1 4.0 7.2 2017 update 5 November 11th 2017 (framework October 17th 2017)
4.7.2 4.0 7.3 2017 update 7 May 7th 2018 (framework April 30th 2018)
4.8 4.0 8.0 2019 update 3 September 19th 2019 (framework April 18th 2019)

.NET Core:

.NET core C# language Visual Studio IDE Release date LTS
1.0 6.0 2015 update 3 June 27th 2016 No
1.1 6.0 2017 November 16th 2016 No
2.0 7.1 2017 update 3 August 14th 2017 No
2.1 7.3 2017 update 7 May 30th 2018 Yes
2.2 7.3 2019 December 4th 2018 No
3.0 8.0 2019 update 3 September 23rd 2019 No
3.1 8.0 2019 update 4 December 3rd 2019 Yes

.NET

.NET C# language Visual Studio IDE Release date LTS
5.0 9.0 2019 update 8 November 10th 2020 No

New features:

Actual new features in .NET 5.0 and C# 9.0 compared to .NET FX 4.8 / .NET Core 3.1 and C# 8.0.

General:

By far the biggest change is the change from .NET FX to .NET Core (see above).

The second biggest change is the addition of record type family.

The remaining changes are minor.

Init only properties:

C# 9.0 adds init only properties.

Syntax:

public type propertyname { get; init; }

C# 8.0 code:

using System;

public class Data1
{
    public int Iv { get; set; }
    public string Sv { get; set; }
}

public class Data2
{
    public int Iv { get; }
    public string Sv { get; }
    public Data2(int xiv, string xsv) // necessary since there are no setters
    {
        Iv = xiv;
        Sv = xsv;
    }
}

public class IOP8
{
    public static void Main(string[] args)
    {
        Data1 o1 = new Data1 { Iv = 123, Sv = "ABC" };
        // possible to change o1 here
        Console.WriteLine($"{o1.Iv} {o1.Sv}");
        Data2 o2 = new Data2(123, "ABC");
        Console.WriteLine($"{o2.Iv} {o2.Sv}");
    }
}

C# 9.0 code:

using System;

public class Data
{
    public int Iv { get; init; }
    public string Sv { get; init; }
}

public class IOP9
{
    public static void Main(string[] args)
    {
        Data o = new Data { Iv = 123, Sv = "ABC" };
        // *NOT* possible to change o1 here
        Console.WriteLine($"{o.Iv} {o.Sv}");
    }
}

Maybe not a super important feature but certainly useful as it does fill a requirement that are frequently occuring.

Relational operator patterns:

C# 9.0 adds relational operator patterns to the switch statement.

Operators:

C# 8.0 code:

using System;

public class ROP8
{
    public static void Test(double v)
    {
        string s;
        if(v < 0.0)
        {
            s = "v < 0";
        }
        else if(0.0 <= v && v <= 1.0)
        {
            s = "0 <= v <= 1";
        }
        else if(1.0 < v)
        {
            s = "1 < v";
        }
        else
        {
            s = "WTF?";
        }
        Console.WriteLine(s);
    }
    public static void Main(string[] args)
    {
        Test(0.5);
    }
}

C# 9.0 code:

using System;

public class ROP9
{
    public static void Test(double v)
    {
        string s = v switch {
            < 0.0 => "v < 0",
            >= 0.0 and <= 1.0 => "0 <= v <= 1",
            > 1.0 => "1 < v",
            _ => "WTF?"
        };
        Console.WriteLine(s);
    }
    public static void Main(string[] args)
    {
        Test(0.5);
    }
}

I suspect this feature will see some usage even though I do not like it - it is so short that I think readability is suffering.

Target typed new:

C# 9.0 makes it optional to supply type name in new if the type name can be inferred from the target.

Syntax:

sometype somevariable = new(arg1,arg2,arg3);

is equivalent to:

sometype somevariable = new sometype(arg1,arg2,arg3);

C# 8.0 code:

using System;

public class X
{
    public string S { get; set; }
    public X(string s)
    {
        this.S = s;
    }
}

public class TTN8
{
    public static void Main(string[] args)
    {
        X o = new X("ABC");
        Console.WriteLine($"{o.S}");
    }
}

C# 9.0 code:

using System;

public class X
{
    public string S { get; set; }
    public X(string s)
    {
        this.S = s;
    }
}

public class TTN9
{
    public static void Main(string[] args)
    {
        X o = new("ABC");
        Console.WriteLine($"{o.S}");
    }
}

I think this feature is totally unnecessary. Adding the type name explicit is little work and make the code more readable.

Script style:

C# 9.0 makes it optional to supply class declaration and Main method declaration for main program.

C# 8.0 code:

using System;

public class HW8
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello world!");
    }
}

C# 9.0 code:

using System;

Console.WriteLine("Hello world!");

I think this feature is totally unnecessary. C# is not a scripting language and adding those few extra lines to the main program is really nothing.

Records:

C# 9.0 adds a record class type.

C# record class is very similar to Kotlin data class, Scala case class and Java 14 record.

A C# record class is a reference type with some value style semantics and the possibility for very compact declaration.

C# 8.0 code:

using System;

public class Data
{
    public int Iv { get; set; }
    public string Sv { get; set; }
}

public class Rec8
{
    public static void Main(string[] args)
    {
        Data o = new Data { Iv = 123, Sv = "ABC" };
        Console.WriteLine($"{o.Iv} {o.Sv}");
    }
}

C# 9.0 code:

using System;

public record class Data(int Iv, string Sv);

public class Rec9
{
    public static void Main(string[] args)
    {
        Data o = new Data(123, "ABC");
        Console.WriteLine($"{o.Iv} {o.Sv}");
    }
}

To explore the differences between struct, class and record see this code:

using System;

public struct Data1
{
    public int Iv;
    public string Sv;
}

public class Data2
{
    public int Iv { get; set; }
    public string Sv { get; set; }
}

public record class Data3
{
    public int Iv { get; set; }
    public string Sv { get; set; }
}

public class RecDet
{
    public static void Main(string[] args)
    {
        Console.WriteLine("**** struct ****");
        Data1 s1 = new Data1();
        s1.Iv = 123;
        s1.Sv = "ABC";
        Console.WriteLine("Value type: " + typeof(Data1).IsValueType);
        Data1 s2 = new Data1();
        s2.Iv = 123;
        s2.Sv = "ABC";
        Console.WriteLine("Equals: " + s1.Equals(s2));
        Console.WriteLine("Same hash code: " + (s1.GetHashCode() == s2.GetHashCode()));
        Data1 s3 = s1;
        s1.Iv = 0;
        Console.WriteLine($"After assignment update: {s1.Iv} {s3.Iv}");
        Console.WriteLine("**** class ****");
        Data2 c1 = new Data2();
        c1.Iv = 123;
        c1.Sv = "ABC";
        Data2 c2 = new Data2();
        c2.Iv = 123;
        c2.Sv = "ABC";
        Console.WriteLine("Equals: " + c1.Equals(c2));
        Console.WriteLine("Same hash code: " + (c1.GetHashCode() == c2.GetHashCode()));
        Data2 c3 = c1;
        c1.Iv = 0;
        Console.WriteLine($"After assignment update: {c1.Iv} {c3.Iv}");
        Console.WriteLine("**** record ****");
        Data3 r1 = new Data3();
        r1.Iv = 123;
        r1.Sv = "ABC";
        Console.WriteLine("Value type: " + typeof(Data3).IsValueType);
        Data3 r2 = new Data3();
        r2.Iv = 123;
        r2.Sv = "ABC";
        Console.WriteLine("Equals: " + r1.Equals(r2));
        Console.WriteLine("Same hash code: " + (r1.GetHashCode() == r2.GetHashCode()));
        Data3 r3 = r1;
        r1.Iv = 0;
        Console.WriteLine($"After assignment update: {r1.Iv} {r3.Iv}");
    }
}

Output:

**** struct ****
Value type: True
Equals: True
Same hash code: True
After assignment update: 0 123
**** class ****
Equals: False
Same hash code: False
After assignment update: 0 0
**** record ****
Value type: False
Equals: True
Same hash code: True
After assignment update: 0 0

C# 9.0 also has a with expression that allows constructing a record instance from another record instance with one or more properties changed:

using System;

public record class Data(int Iv, double Xv, string Sv)
{
    public override string ToString()
    {
        return $"[{Iv},{Xv},{Sv}]";
    }
}

public class WE9
{
    public static void Main(string[] args)
    {
        Data o1 = new Data(123, 123.456, "ABC");
        Console.WriteLine(o1);
        Data o2 = o1 with { Sv = "DEF" };
        Console.WriteLine(o2);
    }
}

This feature is popular in other languages, so I expect it to become popular in C# as well.

nint/nuint:

C# 9.0 adds two new integer types nint and nuint.

They are signed and unsigned int of size 32 bit in 32 bit .NET and size 64 bit in 64 bit .NET.

I think this feature is irrelvant for most .NET developers.

Next:

See .NET 6.0 and C# 10.0 New Features.

Article history:

Version Date Description
1.0 November 15th 2020 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj