.NET Framework is a development platform created by Microsoft and was first released in 2002. It's known for being the best choice for developing desktop and server applications on Windows, thanks to the seamless integration with Microsoft's OS.

In 2016, Microsoft released .NET Core 1.0. It's an open-source and cross-platform rewrite of the .NET Framework, and meant to be the future of the platform. 3 years later, .NET Framework 4.8 was announced as its final major version. Users wanting to benefit from new features would have to migrate to the new .NET Core, which was rebranded as ".NET" starting version 5.0.

Let's go over why .NET can be a great choice for your next backend.

Cloud-ready

Cloud infrastructure providers are now the first choice for hosting a web platform, no matter how complex its architecture is. For your technical stack to be cloud-ready, it must be: Linux-friendly, container-friendly and observable.

Let's see what it means for .NET.

Linux-friendly

In 2014, Microsoft shifted its strategy towards open source, from rejection to adoption, thanks to the newly appointed CEO Satya Nadella. The company is heavily investing in Linux through their public cloud offering, by creating their own distros and contributing to the kernel, and through their mainstream operating system Windows, in the form of Windows Subsystem for Linux.

Microsoft CEO Satya Nadella talking about Linux support in Microsoft Cloud. San Francisco, California October 20, 2014

Due to this new direction, .NET Core was announced back in November 2014 as an open source and cross platform development stack. Today, Red Hat supports .NET and includes it in their Enterprise distribution.

.NET can also be installed on Ubuntu, Debian, Fedora, CentOS and OpenSUSE using their respective package managers. The official documentation has a dedicated section for Linux installations.

Container-friendly

Microsoft provides official docker images for both the runtime and the SDK. All images are available for both ARM and x86 architectures, and are offered in 3 flavors: Alpine, Debian and Ubuntu base images.

It's not just about building base images, changes were made at multiple levels of the .NET software stack so that .NET applications play well with container engines. It ranges from supporting cgroups v2 to providing a container sidecar to access diagnostic information of a .NET process, in addition to optimizing image sizes for multi-stage build scenarios.

Observable

Observability is a must for any application in production, even more in a distributed environment. Metrics give you an overview of your application's health so that you can proactively detect and react to anomalies, while logs and traces can save you many hours or even days when investigating problems.

The Metrics APIs were recently introduced in .NET as an implementation of the OpenTelemetry specifications. This makes it easy to export metrics to tools like Zipkin, Prometheus, Jaeger or any OpenTelemetry Protocol compatible destination.

When using the ASP.NET Core Runtime docker image, all your application logs will by default be printed to the console in a structured JSON format, which is easier and safer to parse and process than simple text logging.

# docker run --rm -it -p 8000:80 mcr.microsoft.com/dotnet/samples:aspnetapp
....
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: http://[::]:80","State":{"Message":"Now listening on: http://[::]:80","address":"http://[::]:80","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrl\u002BC to shut down.","State":{"Message":"Application started. Press Ctrl\u002BC to shut down.","{OriginalFormat}":"Application started. Press Ctrl\u002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"Message":"Hosting environment: Production","envName":"Production","{OriginalFormat}":"Hosting environment: {envName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app/","State":{"Message":"Content root path: /app/","contentRoot":"/app/","{OriginalFormat}":"Content root path: {contentRoot}"}}
An example of structured logs using the aspnetapp sample

Thanks to the logging abstraction built in .NET, you just have to plug your library of choice in order to send the application's logs to Sentry, elmah.io, Graylog, KissLog and more.

Extensible design

.NET provides a set of APIs for commonly used programming patterns and utilities. While widely used in ASP.NET Core applications, these APIs are not coupled to the ASP.NET Core application model.

Implementations for those APIs are provided by Microsoft as well as many third-parties. Thanks to this extensible design, developers can plug and switch between them in a clean and easy way.

Below is a list of the most popular APIs, and some of their implementations (third-parties are marked by *):

  • Microsoft.Extensions.Configuration: JSON, Ini, XML, Yaml*, EnvironmentVariables, CommandLine, Microsoft Azure App Configuration, AWS Systems Manager Parameter Store*, DockerSecrets*
  • Microsoft.Extensions.Logging: Console, Debug, Serilog*, Log4Net*, ApplicationInsights, Azure App Services, Sentry*, GELF*, Datalust Seq*, Elastic APM*, Slack*
  • Microsoft.Extensions.DependencyInjection: Default implementation and Autofac*
  • Microsoft.Extensions.FileProviders: File system, Embedded, WebHDFS*
  • Microsoft.Extensions.Caching.Abstractions: InMemory, SQL Server, Redis, Cosmos DB, PostgreSQL*, MongoDB*, MySQL*, SQLite*

Performance

Based on the TechEmpower benchmark, ASP.NET Core is the fastest popular web framework. It's not a side-effect, but a conscious effort of the whole .NET platform team to enhance performance.

After each release, Stephen Toub, one of the team's senior software engineers, writes a lengthy article detailing all the performance enhancements in the released version (if you want to deep dive: 2.0, 2.1, 3.0, 5.0, 6.0).

Picture from the official .NET website, showing .NET ahead of Java Servlet and Node.js in term of requests per seconds (based on the independent benchmark TechEmpower Round 20)

Many new performance-related features were introduced in the latest .NET 6 release, with the most notable being the Dynamic Profile-guided Optimization (Dynamic PGO). This new feature can improve your application speed by up to 26% with a single environment variable and zero code change.

Let's take a step back and explain static PGO. It is a known technique used in many development stacks that consists in instrumentalizing a first build of your application for profiling purposes, collecting information during its execution, and then using that data to make the best optimization decisions in the consequent build. This manual process is often automated in continuous integration pipelines. With .NET 6 and Dynamic PGO, all those steps are taken care of natively by the runtime just by setting DOTNET_TieredPGO environment variable to 1 during your application execution. Free performance boost !

C# language features

.NET consists of the Common Language Runtime (CLR), an extensive shared library and the supported programming languages (C#, F# and Visual Basic).

We will cover in this section some of the unique features of the C# language.

Null-safety

C# 8 introduced the nullable reference types concept. Before that, all reference types were necessarily nullable. Now they are considered non nullable by default, and the developer has to explicitly declare variables and fields as nullable.

Based on these assumptions, .NET compiler can perform an improved static flow analysis that determines if a variable may be null before dereferencing it. It's a huge help to counter the dreaded consequence of the billion-dollar mistake: NullReferenceException.

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

It's not a bulletproof solution as it has some known pitfalls, but it will certainly lead you to create more reliable applications.

Asynchronous programming

In order to achieve high throughput in web servers, asynchronous programming is a must. The idea is that instead of blocking the main thread on operations like disk and network access, your application can offload that operation to a pool of workers (thread pool), letting the current thread do some other work like handling a new incoming HTTP request. Once the offloaded operation is finished, the application will resume its execution from where it stopped.

In some other development stacks, asynchronous programming requires using callbacks, which can be hard to read and to maintain. In 2012, C# designers added the keywords async/await as a solution to that problem.

Without and with async/await code examples (from the async wikipedia page)

You don't have to sacrifice readability and maintainability for the sake of performance anymore.

This feature inspired many other languages to adopt the await syntax.

Value types

Value types have premium support in C# and .NET. Primitive types like int, float, and double can be used as if they were classes, meaning they have methods, and more importantly, can be specified as types in generic classes. No more boxing and unboxing to create a collection of numbers !

using System;
using System.Collections.Generic;
					
public class Program
{
	public static void Main()
	{
		IList<int> numbers = new List<int>();
		numbers.Add(10); // no boxing
		numbers.Add(20);
		Console.WriteLine($"The last number is {numbers[1]}"); // no unboxing
	}
}

You can also create your own value types by using the struct keyword instead of class. This can be useful for GC-related optimization scenarios, as value type local variables are allocated on the stack instead of the heap.

Allocation-free string and array manipulation

Let's suppose you have to parse a string composed of two integers separated by a comma. Your first try may look like this:

string input = ...;
int commaPos = input.IndexOf(',');
int first = int.Parse(input.Substring(0, commaPos));
int second = int.Parse(input.Substring(commaPos + 1));

It's a straightforward solution that works, but it allocates two new strings in order to parse each one into an integer. In this example, we have just one line with two columns, but imagine a much bigger data volume, thousands or even millions of strings will be created just to be passed to the Parse method of int.

C# provides a more efficient way to process strings and arrays in general, thanks to the Span construct. The previous code can be slightly changed to:

string input = ...;
int commaPos = input.IndexOf(',');
ReadOnlySpan<char> inputSpan = input;
int first = int.Parse(inputSpan.Slice(0, commaPos));
int second = int.Parse(inputSpan.Slice(commaPos + 1));

We obtain the same result, but with zero extra allocation.

Span is used to get a narrowed view of a contiguous region of arbitrary memory, such as strings and arrays. The majority of methods in the platform were overloaded to accept Span, like the Parse method in the example.

Rich ecosystem

What makes a strong developer platform is the community behind it. For example, the npm repository and the huge number of published packages had a big impact on the popularity of javascript.

Nuget is the equivalent of npm in the .NET world. Developers can publish their own packages to complement or enhance features provided by the platform. It hosts ~285 000 packages that were downloaded 147 billions times (as reported in nuget.org homepage).

Some of the most popular packages:

  • Logging: Serilog, nlog and log4net
  • Object-to-object mapping: Automapper
  • Object-relational mapping: EntityFramework and Dapper
  • Testing: xunit, NUnit, Moq and FluentAssertions
  • Others: Refit (type-safe REST client), Polly (resilience and transient-fault-handling library) and MediatR (in-process messaging library)

Release cycle and support

A major .NET version is released every year. During that week, many platform-related announcements are made, like new versions of the C# language, ASP.NET Core, Entity Framework Core and more.

As for the support, the .NET website states:

Even numbered releases are LTS releases that get free support and patches for three years. Odd numbered releases are Current releases that get free support and patches for 18 months.
.NET Release cycle and support (source dotnet.microsoft.com)

Development tools

There was a time when using Visual Studio (full blown version, not Code) was required to develop in .NET, but not anymore. Now the SDK ships with a powerful dotnet CLI that supports all the common operations (initialize a new project, add dependencies, format code, compile, run tests, manage dev certificates and user secrets, ...).

As for the IDE, the defacto standard is still Visual Studio, which provide a seamless development and debugging experience, but it's only available on Windows and more recently macOS. For Linux users, cross-platform alternatives exist such as VS Code as well as the powerful and paid Jetbrains's Rider. Note that Jetbrains is also known for its widely used Visual Studio extension Resharper that many C# developers swear by.

Conclusion

.NET has evolved from being Windows-centric to a truly open-source and cross-platform development stack for all types of applications and workloads. Every year, a new version is released that brings a load of new features, performance improvements and ecosystem enhancements, making the new .NET a safe bet for future-proof application development.