Last Updated: May 22, 2026
A namespace is a named container for types. Every class, struct, interface, enum, and delegate in a C# program lives inside exactly one namespace, and the namespace is part of the type's real, full identity. This lesson covers what namespaces are for, how to declare them, how the using directive lets you stop typing long names, how to alias and fully qualify names when two namespaces collide, and the base class library namespaces common in almost every file.
A medium-sized program has hundreds of types. A large one has thousands. Once you cross even a few dozen, you run into a basic problem: more than one type wants the same short name.
Consider an e-commerce codebase. There's a Product class for items in the catalog. There's another Product somewhere in the reporting code that represents a row in a summary table with totals and counts attached. There's a Product in the third-party shipping library you imported, which is the carrier's idea of a product. All three are valid types, all three are useful, and all three want to be called Product.
Without namespaces, you'd have to invent unique names everywhere. CatalogProduct, ReportingProduct, ShippingProduct. Multiply that across every common word (Customer, Order, Address, Cart) and across every library you pull in, and the naming gets ugly fast.
Namespaces solve this by letting the same short name live in different containers. The full identity of a type is its namespace plus its name. ECommerce.Catalog.Product and ECommerce.Reporting.Product are two completely different types from the compiler's point of view, even though both are called Product. There's no conflict, because the full names are different.
The second job of namespaces is organization. A namespace is also how you tell other people (including future you) where a type fits in the program. ECommerce.Catalog is the catalog code. ECommerce.Orders is the order code. ECommerce.Models is shared data shapes. Open a file and the namespace at the top tells you what part of the system you're looking at, before you read a single class.
Namespaces are a compile-time concept. They have no runtime cost. Every reference to a type is resolved by the compiler to a single concrete type; the namespace is part of the identity that's encoded in the assembly metadata, but no work happens at runtime to "look up" a type by namespace.
The classic syntax is block-scoped: you write namespace, the name you want, and a pair of braces containing the types.
The type's full name is now ECommerce.Catalog.Product. Anywhere else in the codebase, that's how the compiler identifies it. The short name Product only works inside the same namespace, or inside a file that has pulled ECommerce.Catalog in with a using directive.
The dot in ECommerce.Catalog is part of the name, not a path separator. The compiler doesn't care what folder the file lives in. You could put the file at src/Catalog/Product.cs or at everything-in-one-folder/x.cs and the namespace would still be ECommerce.Catalog. The convention in most projects is to mirror the folder structure (the file with namespace ECommerce.Catalog sits in a Catalog/ folder), and modern .NET project templates generate namespaces from folder paths automatically, but the compiler doesn't enforce the match.
You can have multiple types in the same namespace block:
And you can split a namespace across many files. Every file that opens namespace ECommerce.Catalog { ... } is contributing types to the same namespace, regardless of which folder or assembly the file lives in. That's the normal case in a real codebase: Product.cs, Category.cs, and Cart.cs all sit in different files and all declare namespace ECommerce.Catalog.
ECommerce.Catalog is a nested namespace, even though there's only one namespace keyword in the declaration. The dot in the name is the nesting. From the compiler's point of view, ECommerce.Catalog is the namespace Catalog declared inside the namespace ECommerce.
You can also nest by writing two namespace blocks one inside the other. The two forms are equivalent:
Both produce a type called ECommerce.Catalog.Product. Almost every codebase uses Form 1 because it's shorter and indents less. Form 2 still works and appears in some older codebases, but it's not the modern style.
Nesting deeper is just more dots. ECommerce.Catalog.Promotions.SeasonalDiscount is a four-level name: ECommerce, then Catalog inside it, then Promotions inside that, then a class called SeasonalDiscount. Each level is its own namespace, and types in any of them can be opened with their own using directive.
A small tree shows the relationship.
The four orange and teal boxes are namespaces. The green boxes are types that live inside them. ECommerce at the top is the root namespace for the application; it usually contains no types of its own, just sub-namespaces. That pattern (one company or product prefix at the root, then functional areas underneath) is the dominant convention in .NET code, and it's what the BCL does too: System is the root, and System.Collections, System.IO, System.Net are functional areas inside it.
Nesting doesn't grant any access privileges. ECommerce.Catalog.Promotions is not automatically allowed to use the short names of types in ECommerce.Catalog just because it's nested inside it. Each namespace is its own scope. To use Product from inside Promotions, the file needs using ECommerce.Catalog; or the fully qualified name, the same as any other file outside the catalog namespace.
using DirectiveTyping ECommerce.Catalog.Product every time you mention a product would be exhausting. The using directive at the top of a file lets you import a namespace, so the short names of its types become available for the rest of the file.
Without the using ECommerce.Catalog; line, the second line of code would have to be var product = new ECommerce.Catalog.Product(101, "Wireless Mouse", 29.99m);. Same result, more typing. The using System; directive does the same thing for the System namespace, which is why you can write Console instead of System.Console.
A few rules about using directives.
They go at the top of the file, above the namespace declaration. Putting them lower is a compile error in most cases. (You can put them inside a namespace block, which scopes the import to that namespace only, but that's a rarely-used variation.)
They affect only the file they're in. using ECommerce.Catalog; in Cart.cs doesn't affect Order.cs. Each file lists what it imports independently. This is one of the reasons C# code tends to start with a tall stack of using lines.
They import the immediate contents of the namespace, not the nested namespaces. using ECommerce.Catalog; makes Product and Category available as short names, but it does not make Promotions.SeasonalDiscount available as SeasonalDiscount. The nested namespace is a separate import: you'd need using ECommerce.Catalog.Promotions; for that.
They don't pull in types you didn't ask for. The directive only changes how the compiler resolves names in this file; the assembly metadata it produces is the same either way. There's no runtime cost to "importing too much."
Four namespaces are in play. System for Console. System.Collections.Generic for List<T>. ECommerce.Catalog for Product. ECommerce.Orders is imported here even though this snippet doesn't use it; that's harmless but messy, and modern IDEs offer a "remove unused usings" command for exactly this reason.
A diagram makes the shortening clearer.
The using directive doesn't change what the type is. Product and ECommerce.Catalog.Product refer to the same compiled type. The directive just spares you from typing the full name on every reference within the file.
Sometimes the short name is still inconvenient, either because it's long, because two namespaces define the same name, or because the namespace itself is so deeply nested that referring to it is awkward. The using directive has an aliasing form that lets you give a name (or a whole namespace) a short, local alias for the current file.
The syntax is using Alias = SomeNamespace.SomeType; or using Alias = SomeNamespace;. The alias is just a name you make up. It applies only to the current file.
The alias Cart is just a stand-in for ECommerce.Models.ShoppingCart within this file. Every use of Cart in the top-level code compiles to a reference to ECommerce.Models.ShoppingCart. From any other file's perspective, the type is still called ShoppingCart. The alias never escapes the file it's declared in.
You can alias a whole namespace, not just a type. using Promo = ECommerce.Catalog.Promotions; lets you write Promo.SeasonalDiscount instead of the full name. That form is useful when you want the path to be clear (so the reader knows which SeasonalDiscount you mean) but don't want to type the full namespace each time.
The most common practical use of aliasing is conflict resolution, which is the next section's job. The second most common is taming generic type names. The full name of a dictionary mapping product IDs to cart entries is System.Collections.Generic.Dictionary<int, ECommerce.Models.CartEntry>, which is long enough that a file dealing with it heavily often introduces an alias:
Aliases of constructed generic types like that were added in C# 12. Before C# 12, you could only alias whole namespaces or non-generic type names. If you're working in a project targeting an older .NET, the generic alias form won't compile.
Aliases are pure compile-time substitution. The resulting assembly is identical whether you write Cart (via alias) or ECommerce.Models.ShoppingCart (fully qualified). Use them when they make the file easier to read, not because they have any runtime effect.
A fully qualified name is the type's complete identity: the full namespace path plus the type name, with no abbreviations. System.Console. System.Collections.Generic.List<int>. ECommerce.Catalog.Product. You can use the fully qualified name anywhere a short name works, and it doesn't require a using directive at all.
The reason fully qualified names exist (beyond being a fallback when you don't want a using) is that they're unambiguous. There is no question what System.Console refers to. There can never be a second System.Console somewhere else in the codebase; the namespace is part of the identity. When two namespaces both define a type with the same short name, fully qualifying one or both removes the ambiguity entirely.
You'll see fully qualified names used in three situations.
The first is isolated references. If a file uses some type from a namespace exactly once, importing the whole namespace for that one use is more clutter than it's worth. Writing System.Diagnostics.Stopwatch.StartNew() once is fine; adding using System.Diagnostics; at the top because of that single line bloats the import list.
The second is disambiguation. If two imported namespaces both define a Product, you can't refer to either as just Product because the compiler doesn't know which one you mean. Fully qualifying one or both resolves the conflict. That case is what the next section covers in detail.
The third is generated code and tooling. Code generators (source generators, EF Core scaffolding, gRPC stubs) emit fully qualified names everywhere because the generator can't know what using directives the surrounding code happens to have. Fully qualifying every name guarantees the generated code compiles regardless of the file's import list.
That OrderService file doesn't import ECommerce.Catalog at all, but it references ECommerce.Catalog.Product by full name and the code compiles fine. If LookupItem were called in dozens of places, the file would probably import the namespace instead; for a single isolated reference, the fully qualified form is fine.
Importing two namespaces that both define the same short name is the conflict case. It happens more often than you'd think: BCL types like Path, Timer, Color, Action, and Task show up in multiple namespaces, and in a large codebase your own namespaces will collide too.
Consider two namespaces that both define a class called Discount:
Both are valid types. Both are useful. Both want the short name Discount. A file that imports both gets a compile error the moment it tries to use the short name:
The compiler can't pick one for you, and that's the appropriate behaviour: silently choosing one would let real bugs slip in. Three options work, each useful in a different case.
Option 1: fully qualify at the use site. Write out the full name where you reference the type. This keeps both imports in place and resolves the ambiguity case by case.
Verbose, but unambiguous. Use this when the file uses both types and both are equally important.
Option 2: alias one or both. Use the using Alias = ... form to give each its own short name within this file.
This is the conventional approach when both types are used heavily in the same file. The aliases document what each one is for and don't depend on the full namespace path being repeated.
Option 3: drop one import. If the file only uses one of them, don't import the other. The remaining short name resolves unambiguously.
Use this when the file is about one side of the boundary and the other namespace was imported by accident or copy-paste.
A small decision tree helps pick an option.
The decision is about how the file uses the types, not about the types themselves. The same conflict between ECommerce.Catalog.Discount and ECommerce.Promotions.Discount might be handled three different ways in three different files, depending on which types each file actually needs.
There's one more form of disambiguation, even if it's rare: the global:: prefix. Writing global::ECommerce.Catalog.Discount tells the compiler to start the name lookup from the root of all namespaces, ignoring any local aliases or nested namespace scopes. Generated code uses it to be defensive against a user having declared a local namespace or alias that shadows a real one. In hand-written code, it's almost never needed.
What happens if you write a type without any namespace block around it? The type goes into the global namespace, also called the default namespace or the root namespace.
LooseClass has no namespace prefix. Its full name is just LooseClass. From any other file in the same assembly, you can refer to it without a using directive, because there's nothing to import. To be precise: it lives in the global namespace, which is the implicit parent of every other namespace.
You can refer to a globally-namespaced type with the special global:: prefix to make the lookup explicit: global::LooseClass. The prefix isn't required for normal use, but it's helpful when you need to disambiguate against a same-named alias or a nested type. The same prefix can target the root of any imported namespace: global::System.Console is identical to System.Console, but starts the lookup at the global root.
Putting types in the global namespace is not standard practice. Every modern .NET project template puts types under a project namespace by default, and most code style guides flag a top-level type without a namespace as an issue. The reasons are practical. A type in the global namespace conflicts with every same-named type in any other library you import. Tooling that generates documentation, scaffolds tests, or builds package-friendly metadata expects namespaces. Pulling a library's types into a new project assumes the library has namespaces, which is how the consumer scopes them.
The global namespace is also where top-level namespaces live. ECommerce is in the global namespace. System is in the global namespace. Anything you can name without a prefix is one step inside the global namespace. The global root itself has no name; it's the starting point for compiler name resolution.
You'll occasionally see namespace ECommerce { ... } with no further nesting, which puts types directly in the ECommerce namespace. That's fine, and many small libraries do exactly this. The choice of how deep to nest namespaces is a style decision, not a correctness one: a 200-line utility library might be flat (MyLib.Helper, MyLib.Parser), while a sprawling application benefits from functional grouping (ECommerce.Catalog, ECommerce.Orders, ECommerce.Promotions).
The .NET Base Class Library is organized into namespaces too, and the same rules apply: you import what you need, fully qualify when you don't want to import, and use aliases when names collide. A few BCL namespaces show up in nearly every C# file, and recognizing them by name is faster than looking them up each time.
| Namespace | What's in it | When to use it |
|---|---|---|
System | Core types (Console, String, Int32, DateTime, Math, Exception, basic delegates Action, Func, Predicate) | Almost every file |
System.Collections.Generic | Generic collections (List<T>, Dictionary<TKey,TValue>, HashSet<T>, Queue<T>, Stack<T>) | Any code working with collections of typed data |
System.Linq | LINQ extension methods (Where, Select, OrderBy, GroupBy, Sum, First, Any, ToList, ToArray) | Any code querying or transforming sequences |
System.IO | File and directory APIs (File, Directory, FileInfo, Path, Stream, StreamReader, FileStream) | Reading from or writing to the filesystem |
System.Text | Text handling (StringBuilder, Encoding, Regex is one namespace over in System.Text.RegularExpressions) | Heavy string construction, encoding conversions |
System.Threading.Tasks | The async/await foundation (Task, Task<T>, ValueTask, Parallel) | Anything async |
System.Net.Http | HttpClient and friends | Calling external HTTP services |
System.Collections (non-generic) | Older non-generic collections (ArrayList, Hashtable) | Mostly legacy; new code uses the generic versions |
A small example uses four of them together:
System provides Console. System.Collections.Generic provides List<T>. System.Linq provides the Where, OrderBy, and Select extension methods. System.IO provides File. Removing any one of those four using lines would break the example with a compile error pointing at the type that's no longer in scope.
Importing a namespace with using has no runtime cost and no measurable compile-time cost. There's no penalty for listing using directives you happen not to use in a given file beyond visual clutter. The IDE's "remove unused usings" command exists for readability, not for performance.
A few of these namespaces have global-using treatment in newer .NET project templates. Since .NET 6, the default Microsoft.NET.Sdk (and the web SDK in particular) emit implicit global using directives for System, System.Collections.Generic, System.IO, System.Linq, and a few others, which is why brand-new C# files in modern projects sometimes have no using lines at all and still work.
A complete two-file example pulls every concept from this lesson into one place.
Several things to take away from this example. The Catalog.cs file declares two namespaces side by side, which is legal: a single file can contribute types to any number of namespaces, although in practice most projects put one namespace per file. The two Product classes are completely different types from the compiler's point of view, despite sharing a short name, because their full names differ. The Program.cs file imports ECommerce.Catalog (so Product would resolve to the catalog version by default) but uses aliases to give each Product an unambiguous local name. The using directives for System, System.Collections.Generic, and System.Linq bring in Console, List<T>, and the LINQ extension methods. The aliases CatalogProduct and PromoProduct only exist in this file; from any other file's perspective the types are still ECommerce.Catalog.Product and ECommerce.Promotions.Product. There's no runtime difference between this and a version that fully qualifies every reference; the aliases are purely a readability decision. And the conflict that would have been a compile error (new Product(...) with both namespaces imported) is resolved by introducing two short, descriptive aliases instead.