C# 9 Top-level Statements In ReSharper and Rider

Khalid Abuhakmeh

With the upcoming release of C# 9, we’re all getting some new tools for our development toolboxes. One of the most exciting features .NET 5 is delivering to developers is top-level statements.

In this post, we’ll explore what top-level statements are, who they’re useful for, the rules of top-level statement apps, and taking a peek at intermediate language (IL) generation.

Gather round the virtual campfire 🏕, and let’s explore this topic together.

What Are Top-Level Statements?

Top-level statements allow us to simplify our application by removing unnecessary ceremony. When we say "ceremony", we are talking about namespaces, program boilerplate, and the idioms of "one class per file." Let’s look at the evolution of a Hello World app from traditional syntax to one that utilizes top-level statements.

using System;

namespace ConsoleApp3
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World!");
    }
  }
}

There are several ceremonial elements in the code above:

  • The namespace of ConsoleApp3
  • The class of Program
  • The application entry point of Main

Wouldn’t it be great if we could start typing the logic of our application?

Well, top-level statements allow us to do just that. Let’s convert our example.

Prerequisites

You’ll need the latest ReSharper 2020.3 EAP or Rider 2020.3 EAP. ReSharper users will also need to use the Visual Studio Preview 2019 to target .NET 5.

Getting Started

To follow along, be sure that the project (.csproj) file of the application using top-level statements has a <LangVersion>preview</LangVersion> element.

<Project Sdk="Microsoft.NET.Sdk">

 <PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net5.0</TargetFramework>
  <LangVersion>preview</LangVersion>
 </PropertyGroup>

</Project>

As our first step, we can be to drop the ConsoleApp3 namespace giving us the following code.

using System;

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("Hello World!");
  }
}

Next, let’s remove the Program class. It’s unnecessary noise in the world of top-level statements.

using System;

static void Main(string[] args)
{
  Console.WriteLine("Hello World!");
}

Finally, let’s remove the Main method, leaving only the Console.WriteLine statement.

using System;

Console.WriteLine("Hello World!");

Woah! It still works! For those who like to live on the edge, we can also reduce our entire application to a single line.

System.Console.WriteLine("Hello World!");

We’ve just written our first top-level statement application.

Who Benefits From Top-level Statements

Well, the short answer is that everyone benefits from top-level statements.

Personally, as a technical blogger, I find top-level statements a more compact way of sharing and conveying ideas to fellow .NET developers through online mediums. Beginners can see the entirety of an application, with a reduction in cognitive noise. Thoughts expressed more clearly, provide higher value to our community.

With recent releases of .NET Core, all ASP.NET applications are hosted within a console executable. Stand-alone web apps are now a more compelling possibility to a broader .NET audience. Using top-level statements, we can simplify our console programs even further, especially useful for folks building microservices and utility apps.

C# 9 continues the long tradition of giving developers options to express and implement their solutions. Top-level statements are an opt-in approach to writing our applications, and with the help of ReSharper, we want folks to give this feature a try.

Besides top-level statements, the latest version of ReSharper and Rider will add support for other C# 9 features that can help us reduce code, catch nullability issues, perform complex refactorings, utilize new syntax, and much more.

Top-level Statements Rules

While the syntax removes many traditional ceremonial constructs, it doesn’t mean a complete disregard for rules. There are rules our application must follow to be considered "correct". Let’s walk through them quickly.

Rule #1: One Entry Point Per Project

Only one file within our application can utilize top-level statements. When a top-level statement file is present, the compiler generates entry point boilerplate at build time.

There are no secrets in .NET, it’s all visible in the intermediate language produced at compile time. Let’s use the IL Viewer tool (ReSharper | Tools | IL Code) to see what the IL is in our simplified Hello World app.

IL Viewer Output
.class private abstract sealed auto ansi beforefieldinit
 '<Program>$'
  extends [System.Runtime]System.Object
{
 .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
  = (01 00 00 00 )

 .method private hidebysig static void
  '<Main>$'(
   string[] args
  ) cil managed
 {
  .entrypoint
  .maxstack 8

  // [3 1 - 3 35]
  IL_0000: ldstr    "Hello World!"
  IL_0005: call     void [System.Console]System.Console::WriteLine(string)
  IL_000a: nop
  IL_000b: ret

 } // end of method '<Program>$'::'<Main>$'
} // end of class '<Program>$'

We don’t need to be IL wizards to see what’s happening. The compiler creates a Program class with an entry point of Main that takes in the familiar string[] of arguments.

If we were to have two top-level statement files in our app, the compiler would not be able to determine the entry point of our app. Additionally, we cannot add a new top-level statement file without first removing any existing entry points in our application.

ReSharper can help in this situation, catching the mistake of multiple entry points and suggesting combining all top-level statements into a single file.

two top-level statement files

Rule #2: Declaration Order Matters

The order of our declarations is essential to the compilation of our top-level statement application. Using directives must be located at the beginning of any top-level statement file. Using directives are followed by our top-level statements. In our case, that would be the line containing the WriteLine method call.

Any methods declared above a top-level statement will be treated as a local function. If we want to play a trick on our developer friends, we could write something like this and ask for their "help". Sure to be a fun prank!

using System;
// looks like an entry point
// but in fact it's just an unused local function! uh-oh!
void Main(string[] args)
{
    Console.WriteLine("wannabe entry point");
}

Finally, we can define any types used in our app at the bottom of the file. ReSharper understands that ordering is critical, offering helpful suggestions to move elements around.

top-level statements order matters

Rule #3: No Limits On Complexity

Top-level statements can utilize the async/await features of C#. That means our applications are not limited in their ability to perform complex tasks or use third-party libraries that perform asynchronous operations. While some folks might see top-level statements as an ideal use-case for scripting and smaller utility apps, it is by no means only for those use-cases. The sky’s the limit.

For those building utilities, top-level statement applications provide access to command-line arguments in the form of an args variable.

using System;

// added "Khalid" to Debug Properties
Console.WriteLine($"Hello {args[0]}");

Running our app provides us with the expected result of Hello Khalid.

Conclusion

Top-level statements are a fun feature of C# 9, and as we’ve seen, it helps reduce ceremonial keystrokes and structure. ReSharper understands and can guide folks starting with top-level statements, helping them follow the rules: one entry-point per app, ordering of statements matters, and the sky’s the limit when it comes to complexity.

Additional tools like the IL Viewer can help understand how the C# compiler transforms our top-level statement application into a runnable executable.

Please try out the latest ReSharper 2020.3 EAP, provide feedback in the comments, and file any problems or concerns in the ReSharper issue tracker.

References

Subscribe

Subscribe to .NET Tools updates