C# 14 New Feature: Script Directives
.NET 10
C# 14 introduces new directives that transform C# into a true scripting language. In this video, Ian Griffiths explains how the .NET 10 SDK now lets you run a single C# source file directly—no project file required.
What you'll learn:
- How to run C# files directly with
dotnet run - The shebang (
#!) directive for Unix/Linux script execution - The new
#:directive for adding NuGet packages and build properties - How these features build on the low-ceremony entry points introduced in C# 9
Contents:
- 00:00 Introduction to C# 14 Scripting Capabilities
- 00:10 Running C# Source Files with .NET 10 SDK
- 00:32 Simplifying C# Program Structure
- 01:47 Shebang Syntax for Unix Systems
- 04:05 Ignored Directives in C# 14
- 04:44 Using External Libraries in C# Scripts
- 07:38 Conclusion
Transcript
C# 14 has added new directives that enable C# to be used as a scripting language, starting with the .NET 10 SDK. The .NET run command now supports being run against a single C# source file, so you no longer need a project file. In this folder, I have just a single C# source file. There's no solution file and no .csproj, just this code. I can supply the file name to the .NET run command, which compiles and runs it. This is a natural progression from the C# 9 feature that enabled us to write the program entry point in this simple way. So whereas once we used to have to declare a containing method and type, now we can omit all of that and write just the statements that run when the program starts, and the compiler fills in the rest.
The basic principle was that we shouldn't have to write a load of boilerplate if the compiler can generate suitable defaults. And now in .NET 10, that same idea extends to the project file. If that was only going to contain the defaults, why not get rid of it completely? So now, for the first time, a complete and runnable C# program can live in a single source file.
So although C# continues to be a compiled language, these changes enable us to use it much more like a normal scripting language. The combination of the feature added back in C# 9 that let us write these low-ceremony entry points, in conjunction with the new .NET 10 SDK support for running a C# source file without needing a project file, enables us for the first time to put an entire C# program into a single runnable source file with no supporting files required. However, for this to be useful, we need some more things. On Unix operating systems, the convention is that a script file begins with what's sometimes known as the shebang syntax. So the idea here is that the very first line of the script will begin with a hash character followed by an exclamation mark, and the operating system detects that well-known starting sequence. It goes, "Ah, you're using a shebang. Right, the rest of that line is going to tell me which program I need to launch to execute your script successfully." So you see this in most shell scripts. You also see it when people are using Python files as scripts, and we need to do the same thing in C#.
I am running in a container here so that I can use Linux, which recognizes the hashbang convention. This particular container has the .NET SDK installed, and you can see this hello.cs file has execute permission set. In the file, we can see that it begins with a shebang line, and that is enough for me to be able to run this.
When I execute this file, the operating system sees that it begins with the hash and exclamation mark, so it knows that it has to run the command that follows, passing in the file path as an argument. So the effect is that this single C# source file becomes a runnable program. Now you do need the .NET SDK to be installed for this to work because the .NET run command has to compile the file to be able to run it, so this won't work if you've installed only the .NET runtime. And this is mostly an SDK feature. It is the .NET run command that's doing all the work here. The C# compiler just ignores that hashbang directive, and if you go and find the language specification for this new feature, its title is "ignored directives." So as far as the compiler's concerned, these things aren't really any different from a comment, but whereas a comment is designed to be read by other developers, the audience for this directive is the operating system. So it's there because Unix-style operating systems recognize this as a standard start to a script file, and they know what to do with it.
Now, C# 14 also defines another kind of ignored directive. You can also put lines that begin with a #:. Now again, the compiler just does nothing with these—it treats them in much the same way as it does the #! syntax. But these are intended for a different audience. So rather than being directed at the operating system, the #: lines are there for the benefit of the .NET run command. And these enable you to control aspects of how the source file is built that might otherwise have required you to add a project file.
For example, suppose we want to write a C# script that uses external libraries. I've got a script here that discovers the latest available version of a particular NuGet package. Specifically, it's looking at the latest version of System.Reactive, the main component of the Reactive Extensions for .NET, which my employer Endjin currently maintains, by the way. So to do this, my script uses the NuGet client SDK. Now that is not built into the .NET runtime. The NuGet client SDK is itself distributed via NuGet.
So to use it in my C# code, normally I'd expect to add a package reference in my .csproj file, but this is a script and the whole point is that it's self-contained. There is no .csproj file, so I need some way to tell the .NET run command that my code needs to use a NuGet package. And you can see that the second line of this script is a #: directive, and the dotnet run command searches for these in the file, and it expects the colon to be followed by text indicating what we're asking it to do. For this line, the word "package" tells the tool that this code uses a particular NuGet package, specifically the NuGet package that provides the NuGet client SDK that this code uses. Notice there's a second #: directive, this time with the text "property." This lets me set build properties. It turns out that the NuGet client SDK relies on reflection-based JSON serialization, a feature that's disabled by default for scripts, which would cause this program to fail with an exception.
But this directive tells the dotnet run command that when it builds our script into executable code, it should act as though I had a .csproj file with a property group setting this property to true. That enables the feature that the NuGet client library requires, and so the script just works.
So from a C# perspective, this is a really simple feature. It's just two new directive types, both of which the compiler completely ignores. This works and it's useful because the .NET SDK is now able to build and run source files directly. So the two directives: the first one is for the operating system's benefit—it enables us to put a #! mark on the first line of code, and Unix-like operating systems recognize that and it enables them to go and find the dotnet run command in order to execute the script. And then the second directive type enables us to provide instructions to the .NET run tool that might otherwise have required us to add a project file.
My name's Ian Griffiths. Thanks for listening.