File-scoped namespaces (C# 10), global usings, implicit usings and using aliases in modern projects.
A file-scoped namespace (namespace Shop.Orders;, C# 10) applies one namespace to the whole file without braces, saving a level of indentation; a file can have only one. A global using (C# 10) applies a using directive to every file in the project. Implicit usings (<ImplicitUsings>enable</ImplicitUsings>, .NET 6 SDK onwards) make the SDK generate a GlobalUsings.g.cs with common namespaces for the project type (for example System, System.Linq, System.Threading.Tasks, and ASP.NET Core namespaces for web projects). You can add or remove entries with <Using Include="..." /> items. global using static and alias directives work too, and C# 12 lets aliases name any type, such as tuples. The risks are hidden dependencies and name clashes, so keep global usings in one known place.
These three features reduce boilerplate at the top of files: file-scoped namespaces (C# 10) remove a level of indentation, global using directives (C# 10) import namespaces once for the whole project, and implicit usings (.NET 6 SDK) generate a standard set of global usings automatically. I'd explain how each works, where to put global usings, how the SDK generates them, global using static and aliases (including C# 12 "alias any type"), and the problems they can cause in large solutions.
namespace Ordering.Application; // applies to the whole file
public sealed class PlaceOrderHandler { ... }
vs the block form namespace Ordering.Application { ... }. Benefits: one less indentation level, cleaner diffs. Rules: only one file-scoped namespace per file; it must come before type declarations; can't be mixed with block namespaces in the same file. Enforce with .editorconfig: csharp_style_namespace_declarations = file_scoped:warning and fix all files with dotnet format.
// GlobalUsings.cs
global using System.Collections.Immutable;
global using Microsoft.EntityFrameworkCore;
global using Ordering.Domain;
global using in any file applies to every file in the project (compilation).GlobalUsings.cs or Usings.cs) per project, so they're discoverable.<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
The SDK generates obj/.../ProjectName.GlobalUsings.g.cs with a set based on the project SDK:
Microsoft.NET.Sdk: System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading, System.Threading.Tasks.Microsoft.NET.Sdk.Web adds 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.Customise with the Using MSBuild item:
<ItemGroup>
<Using Include="Ordering.Domain" />
<Using Include="System.Text.Json" />
<Using Remove="System.Net.Http" />
<Using Include="Ordering.Domain.Money" Alias="Money" />
<Using Include="System.Math" Static="true" />
</ItemGroup>
Using items work even without ImplicitUsings, and can live in Directory.Build.props for solution-wide defaults.
global using static and aliasesglobal using static System.Math; // Max, Min, Round without Math.
global using static Ordering.Domain.Guard; // static helpers
global using Json = System.Text.Json.JsonSerializer; // alias
C# 12 "alias any type" lets aliases name any type, including tuples, arrays, pointers and generic instantiations:
global using Point = (int X, int Y);
global using CustomerMap = System.Collections.Generic.Dictionary<Ordering.Domain.CustomerId, Ordering.Domain.Customer>;
global using unsafe NativeHandle = byte*;
Useful for readability, but aliases are not new types — Point is still a ValueTuple<int,int> with no type safety.
Task vs a domain Task entity, Timer from System.Threading vs System.Timers, File vs a File entity, Thread, Monitor). Errors appear far from where the using was added.Microsoft.EntityFrameworkCore in a shared props file makes EF types available in the domain project, encouraging forbidden dependencies.obj/, invisible in Solution Explorer by default.Round(x) — which class?).Guidelines: keep global usings to genuinely ubiquitous namespaces, one visible file per project, avoid domain/infrastructure global usings in shared props, use aliases and static imports sparingly, and resolve ambiguities with explicit usings or aliases.
Adding global using System.Threading; via a shared Directory.Build.props made Timer ambiguous in ~40 files that used System.Timers.Timer, and a domain project gained access to Microsoft.EntityFrameworkCore through another shared global using — someone then added [Index] attributes to domain entities. We moved global usings into per-project GlobalUsings.cs files, kept shared props minimal, and added an architecture test forbidding EF Core references from the domain assembly.
The interviewer checks familiarity with modern project conventions and their trade-offs.
Green flags
Using MSBuild item and Directory.Build.props.Red flags
Global usings are just using directives with compilation-wide scope. The compiler collects them from all files (including generated ones) before binding names. Ordering across files doesn't matter. Duplicate global usings produce warnings (CS8933) but are harmless.
When a simple name like Task is used, the compiler looks in the current namespace and its parents, then in using directives (global and file-level). If two imported namespaces contain a type with that name and neither is in an enclosing namespace, you get CS0104 (ambiguous reference). Types in enclosing namespaces take precedence over imported ones — a domain Task in Ordering.Domain wins in files inside Ordering.Domain but not elsewhere.
Generated at obj/<config>/<tfm>/<Project>.GlobalUsings.g.cs:
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
...
The global:: prefix avoids ambiguity with nested namespaces.
using Money = Ordering.Domain.Money;.Using item with Alias.using MaybeInt = int?;), but not nullable reference types (string?) as aliases.dotnet format / IDE code cleanup can convert to file-scoped namespaces and remove unnecessary usings (IDE0005 — enable GenerateDocumentationFile or the rule's build enforcement for CI).GlobalUsings.g.cs.Using Remove item and see which files break.Task in your domain and observe CS0104 in files using System.Threading.Tasks.<!-- Directory.Build.props: shared across the solution -->
<Project>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Text.Json" />
<Using Include="System.Console" Static="true" />
<Using Remove="System.Net.Http" /> <!-- not wanted in domain projects -->
</ItemGroup>
</Project>
// GlobalUsings.cs (one known place per project)
global using System.Collections.Immutable;
global using Money = decimal; // alias for readability
// Orders/OrderService.cs
using Point = (double Latitude, double Longitude); // C# 12: alias any type
namespace Shop.Orders; // file-scoped namespace
public sealed record Order(int Id, Money Total, Point DeliveryTo);
public static class OrderService
{
public static string Describe(Order order)
{
ImmutableArray<string> tags = ["express", "fragile"];
WriteLine($"Order {order.Id}"); // from global using static System.Console
return JsonSerializer.Serialize(new { order.Id, order.Total, Tags = tags });
}
}
// Program.cs
using Shop.Orders;
var order = new Order(1, 49.95m, (-33.87, 151.21));
Console.WriteLine(OrderService.Describe(order));
What happens: global using lines in random files.
Why it is wrong: hard to find what's imported.
Do instead: one GlobalUsings.cs per project.
What happens: Directory.Build.props adds EF Core or ASP.NET namespaces to every project.
Why it is wrong: erodes layering and causes ambiguities.
Do instead: keep shared usings minimal; add per project.
What happens: sprinkling System.Timers.Timer everywhere.
Why it is wrong: noise; root cause remains.
Do instead: remove the conflicting global using or add an alias.
global using staticWhat happens: many static imports. Why it is wrong: unclear origin of methods. Do instead: limit to very common helpers (or none).
What happens: using OrderId = System.Guid;.
Why it is wrong: still a Guid; mix-ups compile.
Do instead: strongly typed ID types.
What happens: some files block-scoped, some file-scoped.
Why it is wrong: inconsistent formatting.
Do instead: enforce file-scoped via .editorconfig and dotnet format.
What happens: moving code from a web project to a class library breaks compilation.
Why it is wrong: web SDK imports more namespaces.
Do instead: add explicit usings or align Using items.
What happens: leftover usings after enabling global usings. Why it is wrong: noise and confusion. Do instead: run IDE0005 cleanup.
These features are small individually but shape every file in a solution, so they deserve a team decision rather than personal taste. A sensible baseline: file-scoped namespaces enforced by .editorconfig, implicit usings on, a short list of extra global usings in Directory.Build.props, and a rule that domain projects do not get web or HTTP namespaces globally, which helps keep layers clean.
| Decision | Recommendation |
|---|---|
| Namespace style | file-scoped, enforced by IDE0161 |
| Implicit usings | on for apps; consider off for libraries with strict public surfaces |
| Extra global usings | few, central, reviewed |
| Aliases | local readability only |
It is also worth knowing for code reviews and migrations: tools such as dotnet format can convert namespaces across a solution in one commit, which keeps the noisy diff separate from real changes.
MSBuild is the build engine under dotnet build, Visual Studio and Rider, and a .csproj is an MSBuild file. Properties are named values such as $(Configuration). Items are lists with metadata, such as @(Compile) holding every .cs file. Targets are named steps, ordered with DependsOnTargets, BeforeTargets and AfterTargets. Each target runs tasks, the real units of work, such as Csc to compile or Copy. The build has two phases. Evaluation reads imports, properties and items. Execution runs the targets. Targets that declare Inputs and Outputs are skipped when nothing has changed, which is what makes builds incremental.
C# tuples like (int Id, string Name) are System.ValueTuple structs with public, mutable fields Item1 and Item2. The names you write exist only at compile time: they're stored in a TupleElementNames attribute, so reflection, serialisers and dynamic only see Item1 and Item2. Tuples have value equality and support ==, and since names aren't part of the type, (int A, int B) converts to (int X, int Y). System.Text.Json ignores fields by default, so a tuple serialises as {}. I use tuples for private multiple returns, and records for anything public or serialised.
Last updated 30 Oct 2026.