C# Top-Level Statements — File-Scoped Namespaces, Global Usings, and Simplified Program Entry
In this tutorial, you will learn about C# Top. We cover key concepts, practical examples, and best practices to help you master this topic.
C# top-level statements (C# 9+) allow writing code directly at the file level without wrapping in a class or Main method, with file-scoped namespaces and global usings further reducing ceremony.
What You'll Learn
You will master top-level statements in C#: how the compiler generates the Main method automatically, the implicit args parameter, file-scoped namespaces introduced in C# 10, global usings for project-wide imports, and best practices for organizing .NET programs.
Why It Matters
Top-level statements make C# more approachable for beginners, scripts, and small programs. They eliminate the boilerplate class and Main method that confused newcomers. For experienced developers, they reduce ceremony in program entry points, demo code, and simple tools. File-scoped namespaces reduce indentation throughout your codebase.
Real-World Use
ASP.NET Core 6+ uses top-level statements in Program.cs. Console utilities use them for concise entry points. Script-like data processing benefits from reduced ceremony. Demo and tutorial code is more readable without nested classes. Global usings are standard in all modern .NET project templates.
Learning Path
graph LR
A["32: Index Ranges"] --> B["33: Top-Level Statements"]
B --> C["34: File I/O"]
C --> D["35: Serialization"]
D --> E["36: XML LINQ"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Top-Level Statements Basics
// Program.cs - entire program
Console.WriteLine("Hello, World!");
int sum = Add(5, 3);
Console.WriteLine($"Sum: {sum}");
static int Add(int a, int b) => a + b;
The compiler generates:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
int sum = Add(5, 3);
Console.WriteLine($"Sum: {sum}");
static int Add(int a, int b) => a + b;
}
}
Implicit args
// args is implicitly available
Console.WriteLine($"Arguments: {args.Length}");
foreach (var arg in args)
Console.WriteLine($" {arg}");
// Simple CLI tool
if (args.Length == 0)
{
Console.WriteLine("Usage: myapp <name>");
return 1; // Return exit code
}
Console.WriteLine($"Hello, {args[0]}!");
return 0;
File-Scoped Namespaces (C# 10)
// Traditional: indented block
namespace MyApp.Data
{
public class UserRepository
{
// ...
}
}
// File-scoped: no indentation, no braces
namespace MyApp.Data;
public class UserRepository
{
// One less level of indentation
}
public class ProductRepository
{
// Also in MyApp.Data namespace
}
File-scoped namespaces apply to all types in the file. Only one file-scoped namespace per file is allowed.
Global Usings
// In a file like GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using Microsoft.Extensions.Logging;
// These are now available in every file in the project
Implicit Usings
Modern .NET projects automatically include global usings based on the SDK:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
For Microsoft.NET.Sdk (console, class library):
- System, System.Collections.Generic, System.IO, System.Linq
- System.Net.Http, System.Threading, System.Threading.Tasks
For Microsoft.NET.Sdk.Web:
- Above plus: Microsoft.AspNetCore.Builder, Microsoft.AspNetCore.Hosting
- Microsoft.AspNetCore.Http, Microsoft.Extensions.DependencyInjection
For Microsoft.NET.Sdk.Worker:
- Above console plus: Microsoft.Extensions.Hosting, Microsoft.Extensions.Logging
Async Top-Level Statements
// Program.cs can be async
Console.WriteLine("Downloading...");
using var client = new HttpClient();
string content = await client.GetStringAsync("https://example.com");
Console.WriteLine($"Downloaded {content.Length} characters");
// Local functions at file scope
static async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
Mixing Top-Level with Traditional Code
// Program.cs (top-level)
var service = new MyService();
service.Run();
// MyService.cs (traditional class)
namespace MyApp;
public class MyService
{
public void Run() => Console.WriteLine("Service running");
}
Only one file can have top-level statements. Other files use traditional class/struct/record declarations.
Program Structure with Top-Level Statements
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// 1. Setup
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddHostedService<Worker>();
// 2. Build
var host = builder.Build();
// 3. Run
await host.RunAsync();
// Local factory methods
static void ConfigureServices(IServiceCollection services)
{
services.AddLogging();
}
Common Mistakes
Mistake 1: Multiple Files with Top-Level Statements
Only one file in a project can contain top-level statements. The compiler error CS8803 is clear about this. All other files must use the traditional class structure.
Mistake 2: Not Using File-Scoped Namespaces
Block-scoped namespaces add unnecessary indentation. Use file-scoped namespaces (namespace X;) in all new files unless you need multiple namespaces in one file.
Mistake 3: Disabling Implicit Usings Without Adding Alternatives
If you set <ImplicitUsings>disable</ImplicitUsings>, you must add explicit using statements for common types like List<T>, HttpClient, and Task.
Mistake 4: Mixing Top-Level Statements with Explicit Main Method
If you use top-level statements, you cannot have a Main method in the same project. The compiler generates it automatically. Remove any handwritten Main methods.
Mistake 5: Assuming Top-Level Code Runs Before Static Constructors
Top-level statements are generated into the Main method, which runs after static constructors. Order your initialization accordingly.
Mistake 6: Overusing Static Local Functions in Top-Level Code
Local functions in top-level statements must be static (they cannot capture variables). If you need closures, define them as lambdas or use traditional methods.
Practice Questions
- How does the compiler handle top-level statements? What code is generated?
- What is the difference between file-scoped and block-scoped namespaces?
- How do global usings work? Where should you define them?
- Can top-level statements be async? How?
- Create a complete Program.cs that reads a file, processes it, and writes output using top-level statements.
Challenge
Create a complete console application using only top-level statements that implements a simple HTTP server using HttpListener. The server should respond to requests with a status page. Use only top-level statements and static local functions.
FAQ
Mini Project
Create a data processing tool using top-level statements:
// DataProcessor.csproj must have <ImplicitUsings>enable</ImplicitUsings>
using System.Text.Json;
Console.WriteLine("=== Data Processor ===");
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: dataproc <input.json> <output.csv>");
return 1;
}
string inputPath = args[0];
string outputPath = args[1];
if (!File.Exists(inputPath))
{
Console.Error.WriteLine($"Input file not found: {inputPath}");
return 2;
}
Console.WriteLine($"Reading: {inputPath}");
// Read and parse JSON
string json = await File.ReadAllTextAsync(inputPath);
var records = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(json);
if (records == null || records.Count == 0)
{
Console.Error.WriteLine("No records found");
return 3;
}
Console.WriteLine($"Loaded {records.Count} records");
// Generate CSV
await using var writer = new StreamWriter(outputPath);
// Header
var headers = records[0].Keys.ToArray();
await writer.WriteLineAsync(string.Join(",", headers));
// Rows
foreach (var record in records)
{
var values = headers.Select(h =>
{
if (!record.TryGetValue(h, out var element))
return "";
string value = element.ValueKind switch
{
JsonValueKind.String => element.GetString() ?? "",
JsonValueKind.Number => element.GetRawText(),
_ => element.GetRawText()
};
if (value.Contains(',') || value.Contains('"'))
value = $"\"{value.Replace("\"", "\"\"")}\"";
return value;
});
await writer.WriteLineAsync(string.Join(",", values));
}
Console.WriteLine($"Written: {outputPath}");
return 0;
Create a sample JSON file (sample.json):
[
{"Name": "Alice", "Age": 30, "City": "NYC"},
{"Name": "Bob", "Age": 25, "City": "LA"},
{"Name": "Charlie", "Age": 35, "City": "Chicago"}
]
Run:
dotnet run -- sample.json output.csv
What's Next
You have mastered top-level statements in C#. The next lesson covers file I/O: File, StreamReader/Writer, async I/O, and the Path class.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro