TL;DR: Take a look at the new features available in NET Core 3.0. This post will provide you with an overview of the latest release of the well-known Microsoft development platform.
“.NET Core 3.0 is out! Discover the new features provided by the framework.”
Tweet This
.NET Core 3.0 and the .NET Unification
After more than 12 years from its first release, .NET has reached a crucial point in the ecosystem transformation announced last May: the .NET platform unification into .NET 5. This crucial point is represented by the release of .NET Core 3.0 launched these days at .NET Conf.
In recent years, continuous innovation and the desire to bring .NET to platforms other than Windows has created fragmentation in the ecosystem. Developers had to deal with different targets, such as .NET Framework, .NET Core, Mono and so on. The .NET Standard should have put the house in order, but maybe it risked complicating things. The .NET unification aims to simplify the .NET development independently from the target system.
The preliminary efforts for this unification converge in the .NET Core 3.0 release bringing interesting new features and improvements. The new release also implements the latest .NET Standard 2.1 specifications. Far from covering all the new stuff, in this article, you will take a look at the most relevant of them.
Note: In order to leverage the latest features of .NET Core 3.0 while developing your applications, you should use the .NET Core 3.0 SDK or Visual Studio 2019 16.3 or Visual Studio 2019 for Mac 8.3.
Executables and Performance Improvements
A few features of the new release aim to simplify application packaging and delivery.
For example, you can build a single-file executable containing all the dependencies required to run your application in a specific platform. This can be accomplished by using the
PublishSingleFile
flag with the dotnet publish
command, as shown in the following example:dotnet publish -r win10-x64 /p:PublishSingleFile=true
This command will create a single file package for your Windows 10 application.
Alternatively, you can edit your project file by adding the
PublishSingleFile
element, as follows:<PropertyGroup> <RuntimeIdentifier>win10-x64</RuntimeIdentifier> <PublishSingleFile>true</PublishSingleFile> </PropertyGroup>
In addition, .NET Core now creates smaller executables by building framework-dependent executables by default. This means that
dotnet build
and dotnet publish
commands create an executable that matches the platform of the .NET Core SDK you are using, avoiding to include .NET Core itself.Some improvements relate to performance. In addition to some internal code optimization, now the tiered compilation is active by default. This feature helps the Just-In-Time (JIT) compiler get better performance by adapting the generation of optimized code to the various stages of the execution of an application. For example, at startup, you can tolerate the generation of a non-optimized code since you are giving priority to the application responsiveness.
By the way, you can also improve the startup time of your application by compiling it in the ReadyToRun (R2R) format. Binaries built in this format reduce the amount of work of the JIT compiler, but they are larger. If you want to take advantage of it, you need to add the
PublishReadyToRun
key to your project configuration:<PropertyGroup> <PublishReadyToRun>true</PublishReadyToRun> </PropertyGroup>
Then, you need to publish your application as a self-contained app. You can do this by running the
dotnet publish
command with the --self-contained
flag, as shown in the following example:dotnet publish -c Release -r win-x64 --self-contained true
C# 8 Enhancements
One of the most awaited features of .NET Core 3.0 is the latest version of C#. In addition to some internal improvements to the compiler, C# 8 brings a few interesting new features to the language.
Switch Expressions
Switch expressions are a very convenient way to map information in a compact fashion. Take a look at the following code:
public int Calculate(string operation, int operand1, int operand2) { int result; switch (operation) { case "+": result = operand1 + operand2; break; case "-": result = operand1 - operand2; break; case "*": result = operand1 * operand2; break; case "/": result = operand1 / operand2; break; default: throw new ArgumentException($ "The operation {operation} is invalid!"); } return result; }
It defines the
Calculate()
method that performs the four arithmetic operations based on the given arguments. In particular, the operation
string is analyzed through a switch
statement in order to detect the operation to perform.You can rewrite this method in a more compact way as shown in the following code:
public int Calculate(string operation, int operand1, int operand2) { var result = operation switch { "+" => operand1 + operand2, "-" => operand1 - operand2, "*" => operand1 * operand2, "/" => operand1 / operand2, _ => throw new ArgumentException($ "The operation {operation} is invalid!"), }; return result; }
As you can see, the
switch
keyword acts as an operator between the variable to check (operation
in the example) and the map of expressions to evaluate. The resulting code is much more readable since you are now able to catch at a glance which expression matches the operation.“The new C# 8 switch expressions are a very convenient way to map information in a compact fashion”
Tweet This
Nullable Reference Types
One of the most common causes of bug is the null reference exception, due to the attempt to access a
null
value while you expect a non-null value. This type of exception comes out from the lack of a clear declaration of the developer intent. In other words, if you declare a variable without initializing it, like in the following example, its default value in C# before version 8 is null
:string myString;
This changes in C# 8. Any variable of a reference type is now considered a non-nullable reference type. That means that the compiler will emit a warning if the variable is not assigned a non-null value.
If you want your variable have
null
values, you must explicitly declare it as a nullable reference type by appending the well-known ?
at the type name, as shown below:string? myString;
This change will have an impact on your existing code, even if just in helping you to easily find potential bugs. You can read this tutorial in order to migrate your application to the nullable reference type system.
Default Interface Members
This feature allows you to define a default implementation for a member of an interface. To understand how it works, consider the following example of an interface definition:
enum LogLevel { Information, Warning, Error } interface ILogger { void Log(LogLevel level, string message); }
This code defines a very simple interface for a typical logger. You can define your own implementation with the following code:
class ConsoleLogger : ILogger { public void Log(LogLevel level, string message) { Console.Write($"{level}: {message}"); } }
What happens if you add a new member to the interface? Of course, you are breaking the current implementations of that interface. With C# 8 you can define a default implementation for a member so that your existing implementations are preserved. The following is an example of an interface declaration that defines the
LogError()
method with a default implementation:interface ILogger { void Log(LogLevel level, string message); void LogError(string message) { Log(LogLevel.Error, message); }; }
Your
ConsoleLogger
class will inherit the default implementation for the LogError()
method.Asynchronous Streams
Asynchronous streams, also called async streams, allow you to create and consume data streams that may be retrieved or generated asynchronously. .NET Core 3.0 implements some new interfaces introduced in .NET Standard 2.1 in order to support this type of stream. C# 8 allows you to leverage them by using the new
IAsyncEnumerable<T>
type in combination with the async
modifier, the await
keyword, and the yield return
statement.To take a very quick look at the async streams feature, consider the following snippet of code:
public async IAsyncEnumerable<int> GetRelevantDataAsync() { Random random = new Random(); for (int i = 0; i < 1000; i++) { await Task.Delay(100); int randomInteger = random.Next(0, 100); if (randomInteger > 75) { yield return randomInteger; } } }
The
GetRelevantDataAsync()
method defines an asynchronous stream. It generates a random integer every 100 milliseconds but returns only numbers greater than 75.The following code consumes the asynchronous stream generated by the
GetRelevantDataAsync()
method:await foreach (var relevantInteger in GetRelevantDataAsync()) { Console.WriteLine(relevantInteger); }
Note the
await foreach
statement. It allows you to enumerate the asynchronous sequence of integers.Other interesting features also come with C# 8. See this document to learn more.
Improving ASP.NET Core
Along with some clean-up and updates of various templates, ASP.NET Core comes with a few new features that developers should be aware of. Here are some of them.
New Options for MVC Service Registration
Three new extension methods for MVC service registration are now supported:
. This method is used when you are building a web API and don't need any server-side viewsAddControllers
. You use this method when you are building an application that needs a web API and server-side viewsAddControllersWithViews
. You need this method when your application uses Razor Pages — that is, pages using the Razor template markup syntax.AddRazorPages
These extension methods should be used inside the
ConfigureService()
method of the Startup
class of a ASP.NET application, as shown in the following example:public void ConfigureServices(IServiceCollection services) { services.AddControllers(); }
They replace the
AddMvc()
method allowing more granular support based on the type of application you are building. However, the AddMvc()
method has not been removed and it still works as usual.Endpoint Routing
In order to improve the endpoint routing process, it has been decoupled from the MVC middleware. Now you should use the
UseEndpoints()
method of the application instance instead of the UseMvc()
method. The following is an example that uses it in the body of the Configure()
method of the Startup
class:public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
In this case, you assume that your application is just implementing a Web API and you map its controllers by using the
MapControllers()
method. If you are using Razor Pages, then you should use the MapRazorPages()
method.Blazor Improvements
Blazor, the Web UI framework you can use to create Web pages with ASP.NET, has some new features:
- Authentication and authorization support. You now have built-in support for handling authentication and authorization in ASP.NET applications using Blazor.
- Case-sensitive component binding. Components defined in
files are now case-sensitive. This helps the Razor compiler to provide better diagnostic data and allows you to overcome some tricky development scenarios..razor
Culture aware data binding. Now, data-binding for
elements takes into account the current culture as specified by the<input>
property. This means that values will be formatted for display and parsed by using the current culture.System.Globalization.CultureInfo.CurrentCulture
Razor Pages support for
. You can now use the@attribute
directive in Razor Pages in order to specify attributes the same way you are doing with C# classes and members. For example, you can now specify that a page requires authorization in the following way:@attribute
@page @attribute [Microsoft.AspNetCore.Authorization.Authorize] <h1>Protected page<h1>
Of course, these are just a few of the new features that have been added to Blazor.
Other features
Many other features regarding ASP.NET Core are available with the new release. It would be impossible to talk about all them here, but the following should be at least mentioned:
andBodyReader
. These new members of theBodyWriter
andHttpRequest
classes respectively allow you to leverage the high performance ofHttpResponse
. This actually exposes to all developers the technology originally introduced for internal use in the Kestrel Web server.System.IO.Pipelines
- Worker Service template. This is a new project template for long-running tasks such as Windows Services or Linux daemons.
- gRPC support. Thanks to this new feature, you can build lightweight microservices or point-to-point real-time services with ASP.NET Core.
Supporting Windows Desktop Applications
Maybe the most evident effort towards the .NET unification is the support for Windows desktop applications. With this addition, .NET Core 3.0 allows you to run new and existing Windows desktop applications using the UI framework of your choice.
In fact, the new release brings Windows Forms (WinForms) and Windows Presentation Foundation (WPF) to .NET Core. In addition, you will be able to include Universal Windows Platform (UWP) controls to your WPF and WinForms applications by using the so-called XAML Islands.
You will have new .NET Core project templates in Visual Studio and will be able to create a new Windows Forms and WPF project via the
dotnet
command as shown in the following example:dotnet new winforms dotnet new wpf
Microsoft provides instructions to port your Windows Forms and WPF desktop applications from .NET Framework to .NET Core 3.0.
“.NET Core 3.0 allows you to run new and existing Windows desktop applications using the UI framework of your choice”
Tweet This
The New Built-in JSON Engine
This new release of .NET Core no longer relies on Json.NET to serialize and deserialize JSON data. A new JSON serialization library has been introduced in the
System.Text.Json
namespace. The built-in library offers high performance and low memory footprint and is the default library for the project templates involving JSON manipulation, such as the ASP.NET Core templates.The library provides four types:
. It is a high-performance, forward-only reader for UTF-8 encoded JSON text. It should be considered a low-level reader useful to build custom JSON parsers.Utf8JsonReader
. This is the writing counterpart ofUtf8JsonWriter
.Utf8JsonReader
. Based on theJsonDocument
class, this class allows you to parse JSON data and get a read-only DOM that you can navigate and query.Utf8JsonReader
. This static class provides generic methods to serialize and deserialize JSON data.JsonSerializer
By way of example, the following code serializes an anonymous object by using the
Serialize()
method of JsonSerializer
class:var book = new { Author = "John Smith", Title = "Discovering .NET Core 3.0", Price = 48.00 }; var serializedBook = System.Text.Json.JsonSerializer.Serialize(book);
HTTP/2 Support
Support for HTTP/2 has been added to the
class. The default protocol remains HTTP/1.1, but you can enable HTTP/2 in two ways: at the HttpClient
HttpClient
instance level or at the HTTP request level.For example, the following code creates an
HttpClient
instance using HTTP/2 as its default protocol:var client = new HttpClient() { BaseAddress = new Uri("https://localhost:5001"), DefaultRequestVersion = new Version(2, 0) };
On the other hand, the following example shows how to make a single request using the HTTP/2 protocol:
var client = new HttpClient() { BaseAddress = new Uri("https://localhost:5001") }; using (var request = new HttpRequestMessage(HttpMethod.Get, "/") { Version = new Version(2, 0) }) using (var response = await client.SendAsync(request)) Console.WriteLine(response.Content);
Remember that HTTP/2 needs to be supported by both the server and the client. If either party doesn't support HTTP/2, both will use HTTP/1.1.
Aside: Securing ASP.NET Core 3.0 with Auth0
Securing ASP.NET Core 3.0 applications with Auth0 is easy and brings a lot of great features to the table. With Auth0, you only have to write a few lines of code to get solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).
On ASP.NET Core 3.0, you need to create an API in your Auth0 Management Dashboard and change two things on your code. To create an API, you need to sign up for a free Auth0 account. After that, you need to go to the API section of the dashboard and click on "Create API". On the dialog shown, you can set the Name of our API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".
After that, you have to add the call to
services.AddAuthentication
in the ConfigureServices
method of Startup
:string domain = $"https://{Configuration["Auth0:Domain"]}/"; services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.Authority = domain; options.Audience = Configuration["Auth0:Audience"]; });
You also need to add an invocation to
app.UseAuthentication()
in the body of Configure()
method of Startup
.And add the following element to
appsettings.json
:{ "Logging": { // ... }, "Auth0": { "Domain": "bk-samples.auth0.com", "Audience": "http://books.mycompany.com" } }
Note that the domain, in this case, has to be changed to the domain that you specified when creating your Auth0 account.
Recap
In this article, you explored some of the new features of .NET Core 3.0. You read about application packaging and performance improvements provided by the platform and discovered switch expressions, asynchronous streams, and some other new features brought by C# 8.
You also learned how ASP.NET Core is going to replace some of the functionalities of ASP.NET MVC and which improvements are available in Blazor. You read about the new support of Windows Forms and WPF to port Windows desktop applications and peeked into other interesting new features.
Of course, it was impossible to cover all the new features of .NET Core 3.0 in one article, but the ones you discovered here give you an idea of what is going on in the .NET Core evolution.
About the author
Andrea Chiarelli
Principal Developer Advocate
I have over 20 years of experience as a software engineer and technical author. Throughout my career, I've used several programming languages and technologies for the projects I was involved in, ranging from C# to JavaScript, ASP.NET to Node.js, Angular to React, SOAP to REST APIs, etc.
In the last few years, I've been focusing on simplifying the developer experience with Identity and related topics, especially in the .NET ecosystem.