If you haven’t looked at .NET related technologies recently, you should give it a go with .NET 6, which was released just a couple of months ago (November, 2021). One common theme that has emerged with this release is minimalism. While historically, .NET has been a boilerplate heavy framework, recent enhancements to the C# language, such as top-level statements, and sensible defaults and conventions in the .NET framework has removed a lot of that setup heaviness giving way to clean, minimal, beautifully elegant code files. Let’s look at a couple of examples.
Hello World Console Application in .NET 6
First, double-check and confirm that you have v6 or later, of the .NET SDK. If you don’t have it, you can download and install it from here.
In an empty directory of your choice, create a new console app by entering: dotnet new console
If all goes well, you should see a new console project generated for you. If you open the folder in VSCode or navigate to the directory and its contents through your operating system’s file explorer, you’ll see that there is very little, in terms of what was generated. And IMHO, that’s a good thing.
The bin, obj and .vscode folders don’t count as they’re mere artifacts of the code editor and the compilation process. The only two files of note here are really the .csproj
and the Program.cs
files.
The .csproj File
If you’re unfamiliar with .NET apps, the .csproj file (short for C# Project File) serves as a cover page or a manifest of sorts. It contains metadata about your project – such as the type of project or application it is, external packages that are being referenced in it, other projects that are linked to it and other key pieces of data that inform the compiler and related tools on how to build your application. If you are familiar with Node applications, it is akin to the package.json file.
If you have looked at csproj files for applications targeting the “full framework” (the older .NET framework that was built for Windows only as opposed to the new cross-platform, lightweight, blazingly fast .NET), it is quite verbose and ugly. It is often littered with GUIDs of all sorts and references to every single file in your application. The .csproj file that was generated for the “dotnet new console” application looks like this:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Brief. Succinct. Beautiful. Let’s compare that to a .csproj file that was generated for a “hello world” console application using the “full framework,” currently at v4.7.2:
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{AFC0AC6A-EDCF-43C3-AB31-6B4949352EDF}</ProjectGuid> <OutputType>Exe</OutputType> <RootNamespace>ConsoleApp3</RootNamespace> <AssemblyName>ConsoleApp3</AssemblyName> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <Deterministic>true</Deterministic> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PlatformTarget>AnyCPU</PlatformTarget> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PlatformTarget>AnyCPU</PlatformTarget> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Net.Http" /> <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> <Compile Include="Program.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <ItemGroup> <None Include="App.config" /> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> </Project>
Program.cs
The .csproj file is a “package” file. Our actual source code resides in Program.cs. Let’s take a peek at that.
// See https://aka.ms/new-console-template for more information Console.WriteLine("Hello, World!");
Talk about minimal! That’s a single line of code, not counting the comment or the empty line at the bottom! It gets straight to the point, without any ceremony or protocols or precepts. Compare that to the Hello World console application in just the previous version of the .NET SDK, v5:
using System; namespace ConsoleHelloWorld_NET5 { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
Although this older style is just a few more extra lines of code, it’s extra cognitive load and friction for a programmer, especially for one that’s new to the .NET ecosystem. What if the individual is new to programming altogether? One has to content with several programming constructs, some basic and some advanced, just to get to “Hello World” on the screen. Constructs such as “using statements”, namespaces, classes, static vs instance, return types, method parameters, all before outputting a simple greeting to the screen! But don’t get me wrong – those constructs are still all there and they are just as valid and necessary. The framework has been able to hide those by using some new language features in C# and tuck some of those away using some configuration and convention. However, we now have the freedom to bring them in one by one, as needed, on our own terms instead of having to frontload them.
Minimal APIs
The minimal aesthetic is not just limited to console applications; you can stand up a fully functional web API in a similar fashion. In an empty directory, run dotnet new web
. Take a look at the Program.cs file that gets generated with this template:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run();
That’s not pseudo code; it’s a fully functional web endpoint! You can try a dotnet run
to bring it to life and click the URL in the console to bring up the web app in your browser.
If you’ve built APIs in Node using Express or FastAPI or Flask in Python, you’ll quickly find similarities here. To have a similar experience in a blazingly fast framework like .NET is quite spectacular.
Parting Thoughts
Minimal does not mean limited in functionality. It’s an aesthetic. There are pathways to achieve virtually everything that you were doing using full blown Controllers and Startup.cs files in the past using this new paradigm, as well. But, it is certainly not for everyone or for every use case. In my opinion, this new minimal pattern helps immensely in making the .NET Framework and C# language accessible to newcomers. It helps you get them to the “aha moment” quicker with very little friction. This new way also helps in rapid prototyping. You can quickly transform a concept into a functioning thing in record time. But what about building enterprise-grade applications that have to be touched and maintained by many different people and teams? Older, traditional approaches may prove to be a better fit in those cases.