The C# standard calls one source file a compilation unit. A program is the set of compilation units given to the compiler together, and they are compiled as one unit. The order of the files has almost no effect, and no file needs to import another file of the same program (C# specification clauses 6.1 and 14.2).
Encoding and line endings
A .cs file is plain text. The standard requires every compiler to accept
UTF-8 and allows it to accept other encodings. The Microsoft compiler first tries to read
each file as UTF-8. A file in another encoding that uses characters beyond 7-bit ASCII
needs the CodePage option (-codepage), which applies to every
file in the compilation.
The file is turned into a sequence of Unicode characters, then into lines, tokens and directives (clause 6.1). These line terminators are all accepted, and a file may mix them:
| Terminator | Character |
|---|---|
| Carriage return | U+000D |
| Line feed | U+000A |
| Carriage return then line feed | U+000D U+000A, one terminator |
| Next line | U+0085 |
| Line separator | U+2028 |
| Paragraph separator | U+2029 |
Two changes are made at the end of the file, before it is split into tokens (clause
6.3.2). If the last character is Control-Z (U+001A), it is deleted. If the
file is not empty and does not already end in a line terminator, a carriage return is
added, so that the last line is always terminated.
Compilation units
A compilation unit is made of the parts below, in this order. Each part is optional. This grammar is simplified from clauses 14.2, 14.5 and 14.6:
CompilationUnit:
{ExternAliasDirective}
{GlobalUsingDirective}
{UsingDirective}
{GlobalAttributeSection}
{Statement}
{NamespaceMemberDeclaration}
| Part | Example | See |
|---|---|---|
| Extern alias | extern alias Old; | Namespaces |
| Global using | global using System.Text; | below |
| Using directive | using System.IO; | below |
| Assembly or module attribute | [assembly: CLSCompliant(true)] | Attributes |
| Top-level statements | Console.WriteLine("Hi"); | below |
| Namespaces and types | namespace App;, class C { } | Namespaces |
Unlike Java, C# has no one-public-type rule and no link between names and folders. A file may declare any number of public types with any names, and may declare several namespaces. Preprocessor directives may appear between these parts, in lines of their own; see Preprocessor.
// Example.cs
extern alias Legacy; // extern aliases first
global using System.Text; // then global usings
using System; // then using directives
using Legacy::Old.Api;
[assembly: CLSCompliant(true)] // then global attributes
namespace Example; // file-scoped namespace
public class First { }
public class Second { } // any number of public types
Using directives
A using directive makes names from elsewhere available without qualification (clause 14.6). It has no effect on what is linked into the program; the project’s references do that. All using directives in a compilation unit or a namespace must come before any members.
| Form | Example | Effect | Since |
|---|---|---|---|
| Namespace | using System.Text; | Makes the types of that namespace available by simple name | 1.0 |
| Alias | using Sb = System.Text.StringBuilder; | Names one namespace or type; any type from C# 12 | 1.0 |
| Static | using static System.Math; | Makes the static members and nested types of a type available | 6 |
| Global | global using System.Text; | Any of the three forms, for the whole compilation | 10 |
- A using namespace directive imports the types of that namespace only. It does not
import nested namespaces, so
using System;does not bring inSystem.IO. - An alias is visible only in the compilation unit or namespace that declares it. It
may name a constructed generic type,
using L = System.Collections.Generic.List<int>;, but it cannot take type parameters of its own. - From C# 12 an alias may name any type, including tuples, arrays, pointers and
nullable value types:
using Point = (int X, int Y);. A pointer alias needsusing unsafe. - Using directives inside a namespace declaration apply only there.
- A name that two imported namespaces both supply is an error only where it is used.
Global usings
A global using directive (C# 10) applies to every file in the
compilation (clause 14.5). It may use any of the three forms:
global using System.Text;
global using Json = System.Text.Json.JsonSerializer;
global using static System.Console;
GlobalUsings.csGlobal usings must come before every non-global using in the same file.
Implicit usings
Since .NET 6 the SDK can write global usings for you. With
<ImplicitUsings>enable</ImplicitUsings> in the project file, it
adds them to a file it generates in the obj folder. These are the defaults
for each SDK:
| SDK | Namespaces |
|---|---|
Microsoft.NET.Sdk | System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading, System.Threading.Tasks |
Microsoft.NET.Sdk.Web | Those, plus System.Net.Http.Json, Microsoft.AspNetCore.Builder, Microsoft.AspNetCore.Hosting, Microsoft.AspNetCore.Http, Microsoft.AspNetCore.Routing, Microsoft.Extensions.Configuration, Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Hosting, Microsoft.Extensions.Logging |
Microsoft.NET.Sdk.Worker | The Microsoft.NET.Sdk set, plus the four Microsoft.Extensions namespaces above |
Microsoft.NET.Sdk.WindowsDesktop | The Microsoft.NET.Sdk set, plus System.Drawing and System.Windows.Forms for Windows Forms; WPF removes System.IO and System.Net.Http |
File-scoped namespaces
A file-scoped namespace (C# 10) is written namespace Name;. All the types
in the file belong to it, with no extra braces or indentation. See
Namespaces for the full rules.
A file may have only one, it must precede every type in the file, and it cannot be used
alongside top-level statements or block-scoped namespaces.
namespace Shop.Billing;
public class Invoice { }
Top-level statements
Since C# 9, one file in a program may hold statements directly in the compilation
unit, with no Main method (clause 7.1.3). The compiler turns them into the
program’s entry point.
using System.Text;
StringBuilder builder = new();
foreach (var arg in args)
builder.AppendLine(arg);
Console.WriteLine(builder.ToString());
return 0;
class Helper { } // types and namespaces go after the statements
args is available without being declared- Only one file in a project may have top-level statements. The project cannot have
another entry point, and the
StartupObjectoption (-main) cannot be used. - Using directives come first in that file. Types and namespaces come after the statements.
- The statements are in the global namespace.
- The contextual keyword
argsnames the command-line arguments as astring[]. It is never null; it is empty when there are no arguments. - Local variables and local functions declared in the statements belong to the generated method, not to any type.
The signature of the generated entry point depends on what the statements contain:
| The statements contain | Generated entry point |
|---|---|
await and return | static async Task<int> Main(string[] args) |
await | static async Task Main(string[] args) |
return | static int Main(string[] args) |
| Neither | static void Main(string[] args) |
A program written the older way has a static Main method in a class. It may
take string[] or nothing, and may return void,
int, Task or Task<int> (clause 7.1).
File-based apps
From the .NET 10 SDK a single .cs file can be run with no project file:
dotnet run file.cs, or dotnet file.cs, and it may hold
directives for the build, placed at the top of the file. The C# compiler ignores
these directives; the build tools read them. They are listed on the
Preprocessor page.
#!/usr/bin/env -S dotnet --
#:package Spectre.Console@*
using Spectre.Console;
AnsiConsole.MarkupLine("[green]Hello, World![/]");
chmod +x. The SDK documentation recommends LF line endings and no byte order mark for a file with a #! linePartial files
A type may be split across several declarations, usually in several files, by marking
each declaration partial (C# 2, clause 15.2.7). The compiler treats all
the parts as one declaration. Designers and source generators use this to keep generated
code apart from hand-written code.
// Order.cs
public partial class Order
{
public decimal Total { get; }
partial void OnCreated();
}
// Order.Generated.cs
public partial class Order : ISerializable
{
partial void OnCreated() { }
}
Order- All parts must carry
partialand be in the same assembly, module and namespace, with the same name and the same type parameters. partialcan be used on classes, structs, interfaces and records. It must come immediately beforeclass,struct,interfaceorrecord.- Modifiers, attributes, base interfaces and members from every part are combined. A part may repeat a modifier, but parts cannot contradict each other.
- A class has at most one base class, which may be named in more than one part if all name the same one.
Partial members let one part declare a member and another implement it; see Classes for partial methods, properties, indexers, events and constructors.
File-local types
The file modifier (C# 11) limits a type to the file that declares it.
Other files can declare a type with the same name without conflict. It is meant mainly
for source generators.
file class Helper
{
public static void Log(string text) => Console.WriteLine(text);
}
A file-local type must be a top-level type, not nested, and cannot be
partial across different files. It cannot appear in the public signature
of a type that is not itself file-local.
File names and projects
The language places no rules on file names. By convention a file is named after the
type it holds, so Invoice is in Invoice.cs, and the folders
follow the namespaces. Program.cs usually holds the entry point. A
project normally compiles every **/*.cs file below its folder; the
.csproj project file only lists exceptions.
Some file names mark a file as generated code. The compiler applies a disabled nullable context to these, and analysers commonly treat them differently:
- names starting with
TemporaryGeneratedFile_ - names ending in
.designer.cs,.generated.cs,.g.csor.g.i.cs - files with
<auto-generated>or<auto-generated/>in a comment that is the first element of the file - files with
generated_code = truein.editorconfig
The language version a file is compiled with comes from the project, not the file: the
<LangVersion> property sets it, and the default depends on the target
framework. See Versions.