Every C# file used to start with a tall stack of using directives that looked nearly the same across the whole project. System, System.Collections.Generic, System.Linq, System.IO, repeated in file after file. C# 10 added two features that cut most of that boilerplate: the global using directive, and the project-level <ImplicitUsings> switch in the csproj. This lesson covers what each one does, how the compiler resolves a name when both are in play, when global usings make a codebase cleaner, and when they make it harder to read.
global using DirectiveA global using is a using directive that applies to every C# file in the project, not just the file it's written in. You write it once, and the compiler treats every file in the compilation unit as if that namespace were imported at the top.
Those two lines, placed in any .cs file in the project, mean every other .cs file in the same project can use List<T>, Dictionary<K,V>, and LINQ methods like Where and Select without writing using System.Collections.Generic; or using System.Linq; at the top.
The rules for where you can put a global using are strict. Every global using directive must appear before any using directive, any namespace declaration, and any type declaration in the file it lives in. The compiler enforces this; mixing them throws CS8915 ("A global using directive must precede all non-global using directives").
A global using only applies to the project (assembly) it's declared in. It does not leak into other projects that reference this project. If Ecommerce.Core.csproj declares global using System.Linq;, that doesn't mean Ecommerce.Web.csproj, which references Ecommerce.Core, suddenly has LINQ globally available. Each project decides its own globals.
Here is a small example. Imagine the catalog project ships these two files. The first declares the globals.
The second file uses types from those namespaces without any per-file using directives.
Output (when called with a sample catalog):
List<Product>, IEnumerable<Product>, Where, and Console.WriteLine all compile without a single using line in ProductCatalog.cs. The compiler picks them up from the globals declared in GlobalUsings.cs.
GlobalUsings.cs FileA global using is valid in any file in the project. You could put it in Product.cs, in Program.cs, or scatter pieces across a dozen files. None of that is illegal. It is, however, miserable to maintain.
The community convention, and the convention the .NET templates follow, is to put every global using in a single dedicated file at the project root, usually named GlobalUsings.cs. The file contains nothing else, just the directives.
There are two reasons to follow this convention. Discovery is the first. When a new developer joins the project and wonders "where is List<T> being imported from?", they look in one obvious place. Scattering globals across feature files makes that question much harder to answer. The second reason is conflict management. When you later need to remove or modify a global, you only have one file to edit, and a quick grep for global using in the project tells you everything that is implicitly imported.
Program.cs is not the right home for globals even though it technically works. Program.cs in modern C# uses top-level statements, and putting unrelated declarations above the entry point clutters the file you read most often.
global using static and Global AliasesThe using directive has two variants beyond the basic namespace import: using static brings in a type's static members, and using Alias = ... introduces a local name for a type or namespace. Both variants also work with global.
global using static imports a type's static members into every file in the project. After declaring it, you can call static methods on that type without qualifying them.
Round resolves to System.Math.Round and WriteLine resolves to System.Console.WriteLine, because both were imported as static. The file never says Math. or Console..
global using static is convenient, but it has a real cost on readability. A new reader looking at Round(x, 2) has no immediate clue whether Round is a local method, an inherited member, or a static import from somewhere else. Use it sparingly, and only for types whose static members are very common and unambiguous in your domain. System.Math is a common pick; project-specific helper types usually are not.
Global aliases let you rename a type or namespace project-wide.
The aliases ProductId and OrderList are usable in every file in the project. The strength of this pattern is when a primitive type carries a clear domain meaning (ProductId reads better than Guid). The weakness is the same as using static: a reader sees ProductId and may not know it's just a Guid. Keep aliases short, document them in the GlobalUsings.cs file if their meaning isn't obvious, and don't alias a type to a name that already exists somewhere else.
C# 12 expanded what you can alias. Earlier versions only let you alias open generic types and named types; C# 12 added support for tuple types, pointer types, and array types in aliases.
These are useful for tuple-heavy code where the same shape shows up in many signatures.
global using static and global aliases save typing but obscure where names come from. A reader cannot tell from the call site which static class or aliased type is involved. Reserve them for cases where the benefit is clear, and put them all in GlobalUsings.cs so anyone confused has one file to check.
<ImplicitUsings>enable</ImplicitUsings>global using is a language feature. <ImplicitUsings> is a project-level MSBuild feature that builds on top of it. When you enable implicit usings in the csproj, the SDK generates a hidden file at build time containing a curated set of global using directives based on the SDK you're targeting.
When <ImplicitUsings>enable</ImplicitUsings> is set, the build produces a generated file (something like obj/Debug/net8.0/EcommerceApp.GlobalUsings.g.cs) containing a pre-baked list of globals. You never check this file in, you never edit it by hand, and you usually never look at it. It just makes a standard set of namespaces available everywhere.
The exact list depends on which SDK the project uses. For the base Microsoft.NET.Sdk (console apps, class libraries), the implicit usings are:
For Microsoft.NET.Sdk.Web (ASP.NET Core projects), the SDK adds web-specific namespaces on top of the base set:
Microsoft.NET.Sdk.Worker adds the hosting and DI namespaces. The test SDKs add their own sets. The implicit usings match what almost every project of that type needs anyway.
A project with implicit usings enabled does not need a using System; at the top of any file. A Program.cs containing nothing but Console.WriteLine("Hello"); compiles, because System.Console is reachable through the implicit global using System;.
Console, List<T>, and OrderBy are all in scope without any explicit using directives in the file. The Console type comes from System, the List<T> from System.Collections.Generic, and OrderBy is a LINQ extension method from System.Linq. All three are part of the default implicit usings list for Microsoft.NET.Sdk.
New .NET 6+ project templates enable <ImplicitUsings>enable</ImplicitUsings> by default. If you generate a new console app or web app today, that flag is already on, which is why the starter Program.cs files look so minimal. Older projects that have been upgraded to .NET 6+ have the flag off unless someone turned it on manually.
<ImplicitUsings> is not all-or-nothing. The csproj exposes a <Using> MSBuild item type that lets you extend or trim the implicit set on a per-project basis. You add entries to the same <ItemGroup> block.
Each <Using> entry has the same effect as a global using declaration somewhere in the project, but it lives in the csproj instead of a .cs file. The supported attributes mirror the C# syntax:
| Attribute | Equivalent C# | Effect |
|---|---|---|
Include="Ns" | global using Ns; | Adds a namespace to the implicit list. |
Include="T" Static="true" | global using static T; | Imports the static members of type T. |
Include="T" Alias="Name" | global using Name = T; | Defines a global alias. |
Remove="Ns" | (no C# equivalent) | Drops a namespace from the SDK's default implicit set. |
<Using Remove="..."/> is the only way to opt out of a specific implicit namespace without disabling the whole feature. A common use is removing System.Net.Http from a class library that has no business making HTTP calls, both to keep the surface area small and to avoid confusing developers who see HttpClient autocomplete in a project that shouldn't reference the network stack.
There is a stylistic choice here. You can put project-wide globals in GlobalUsings.cs or in the csproj. Both work. The csproj approach has the advantage of being visible in tooling like NuGet diffs and git log on the project file; the C# file approach has the advantage of being editable with normal IDE refactoring tools. Most teams pick one and stick with it. Mixing both is allowed but confusing for the next person reading the project.
Both produce the same effect at compile time. Pick the one your team finds easier to discover.
When the compiler sees a name like List or Console in your code, it walks a defined search order to find which symbol that name refers to. Global usings and implicit usings both feed into that search, but they don't override per-file usings, and per-file usings don't override the enclosing namespace's own declarations.
The order, from highest to lowest priority, is:
using lines at the top of the current file.GlobalUsings.cs, the implicit usings file, or a <Using> item in the csproj.The diagram below shows how these layers combine for a single file in a project with implicit usings enabled.
The three sources on the left, implicit usings, global using directives in C# files, and <Using> items in the csproj, all merge into the same project-wide pool of global imports. The per-file using directives at the top of any given file layer on top of that pool. The final symbol table the compiler sees while compiling that file is the union of everything.
A practical example. Suppose EcommerceApp.Catalog and EcommerceApp.Orders both define a class called Item. The catalog Item is a product on a store shelf; the order Item is a line on an invoice. If the project has global using EcommerceApp.Catalog; and a file adds using EcommerceApp.Orders; at the top, what does the unqualified name Item mean in that file?
The compiler looks at the per-file usings and the globals together. Both expose an Item type. Since the two types compete at the same priority level (they're both reached through using-class imports), the compiler reports CS0104 ("Item is an ambiguous reference between EcommerceApp.Catalog.Item and EcommerceApp.Orders.Item"). The fix is to qualify the name or to add an alias.
There is one case where the layering is not symmetric. If the enclosing namespace itself declares a type with the same name, that wins over both per-file and global imports. So if you're writing code inside namespace EcommerceApp.Orders; and the orders namespace has its own Item, the unqualified Item always refers to EcommerceApp.Orders.Item, regardless of any global usings pulling in another Item. This is the standard C# lookup rule that lets a namespace shadow imported names, and it matters because global usings can quietly fight with each other across feature boundaries when types share names.
A subtler kind of conflict happens when a per-file using and a global using both import the same namespace, or import namespaces that contain colliding type names. The compiler's behavior is sometimes surprising.
Duplicate imports are silently allowed. If System is globally used and a file also writes using System;, that file compiles fine. The duplicate is redundant, not an error. Some IDEs flag it as "unnecessary using" and offer to remove it.
Name collisions raise CS0104, just like collisions between per-file usings. If a global pulls in EcommerceApp.Catalog (with an Item) and a per-file using pulls in EcommerceApp.Orders (with another Item), an unqualified Item reference in that file is ambiguous.
The fix is to remove the per-file using (if you're not actually using anything from that namespace), to fully qualify the name where it's used, or to introduce a per-file alias that picks the variant you want.
The deeper problem this hints at: adding a global using changes the meaning of code in files you didn't touch. If you add global using EcommerceApp.Catalog; to make a few files cleaner, every other file in the project now has EcommerceApp.Catalog in scope, and any unqualified name from that namespace becomes a potential collision with names from other namespaces. The change can break a file that compiled fine yesterday. This is the main reason teams stay conservative about what they add to GlobalUsings.cs.
Adding a global using is a project-wide commit. Every file is now affected, every type in the imported namespace is now a candidate for name lookup everywhere, and a future PR adding another type with a common name can break compilations across many files at once. Be deliberate about what you globalize.
Global usings are a real productivity win when used in moderation. The cases where they help are clear:
The truly universal namespaces. System, System.Collections.Generic, System.Linq, System.Threading.Tasks show up in nearly every file of nearly every project. The implicit usings list is built around exactly these. Importing them globally is a clean reduction in boilerplate that doesn't add ambiguity, because the types in those namespaces (int, string, List<T>, Task, Where) are already part of the mental vocabulary of every C# developer.
A small set of project-specific domain namespaces. If your EcommerceApp.Domain namespace contains the core types (Order, Customer, Product) that almost every feature file references, globalizing it removes a using EcommerceApp.Domain; line from dozens of files. The benefit is real, the risk is contained, and a developer reading the project quickly learns that domain types are always in scope.
Test-only convenience namespaces. In a test project, you often want using Xunit; and using FluentAssertions; (or the equivalent) on every file. Globalizing them in the test project's GlobalUsings.cs keeps test files focused on the actual test rather than the imports.
Here's a reasonable GlobalUsings.cs for a small ecommerce web project:
That's it. No global static imports, no aliases for types that aren't conceptually different from their underlying form, no domain namespaces that only one or two features actually need. The shorter the file, the easier it is for the next reader to absorb.
The same feature, used carelessly, creates problems.
Namespace pollution. Globalizing a namespace makes every type in it a candidate for every name lookup in every file in the project. Adding a new type called Result to a globalized namespace can quietly break files that previously had their own local Result working fine, because the new global candidate creates an ambiguity. The bigger the namespace and the more generic the type names inside it, the higher the risk.
Hard-to-read code for newcomers. A file that starts with a typical block of using directives gives a reader an immediate sense of what the file depends on. A file in an aggressively global-using project may have no explicit imports at all. The reader sees var x = new Item(); and has to guess: is Item defined in the current namespace? Imported globally? Imported through the SDK's implicit list? Aliased to something else? In a project with hundreds of files, this hunt adds up.
Hidden dependencies during refactoring. When you decide to extract a feature into its own project, the new project doesn't automatically inherit the original's global usings. Code that compiled fine in the old project suddenly needs a stack of using directives in the new one. The more aggressively you globalized in the original, the more painful the extraction.
Coupling tests to production globals. A test project that referenced production code through globals can break when the production project changes its globals. Decoupling them (having each project declare its own globals explicitly) limits this kind of cross-project bleed.
The general principle is: globals are a project-level commitment, and the broader the namespace, the higher the cost of regret. A namespace you only intend to use in three files should not be a global.
A few patterns hold up well in real projects:
| Practice | Why |
|---|---|
Put every global using in one GlobalUsings.cs file at the project root. | Discoverability. A single grep answers "what's globally imported?" |
Globalize the universal BCL namespaces (System, System.Collections.Generic, System.Linq, System.Threading.Tasks) and rely on <ImplicitUsings>enable</ImplicitUsings> to do this for you. | Implicit usings exist for exactly this case. No reason to fight them. |
| Globalize project-specific domain namespaces only when most files in the project use them. | Globalizing a niche namespace gives little benefit and adds collision risk. |
Use <Using Remove> in the csproj when an implicit namespace doesn't belong in this project. | Removes irrelevant types from autocomplete and from accidental use. |
Avoid global using static except for System.Math and a small handful of similarly universal types. | Hides where method names come from. Hurts readability more than it helps. |
Keep global aliases to a few domain-level renames (ProductId = Guid). Don't alias common BCL types. | Aliases add a layer of indirection. Justified for domain meaning, rarely for typing convenience. |
When extracting code into a new project, copy the GlobalUsings.cs over too, or restate the imports per-file as part of the extraction. | Globals are project-local. The new project starts empty. |
Don't put global using declarations in feature files. Always centralize them. | Scattered globals are nearly impossible to maintain. |
The pattern most teams converge on: leave <ImplicitUsings>enable</ImplicitUsings> on, add one short GlobalUsings.cs with the few domain namespaces that really are everywhere, leave everything else as per-file using directives, and resist the temptation to globalize anything that isn't already used in most files.
10 quizzes