Assemblies and Namespaces
By the end of this lesson
Explain how compiled code is packaged and how names are organised.
Build a project and a .dll file appears in the output folder. That file is an assembly: one compiled unit, deployed and versioned as a whole.
Inside it are three things. The intermediate language for every method you wrote. Metadata describing every type, member and signature, which is how other assemblies and your editor know what is in there without reading your source. And a manifest listing the assembly's own name and version plus every assembly it depends on.
Namespaces are a completely separate idea. A namespace is a prefix on type names so that two teams can both have an EmployeeValidator without collision. It exists to organise names. It has no file, no version and no presence in your deployment.
People conflate the two because in small projects they line up: one project, one namespace, one .dll. That coincidence stops holding the first time a solution grows past two projects.
The distinction, stated plainly:
| Assembly | Namespace | |
|---|---|---|
| What it is | A compiled file, normally a .dll | A prefix that groups type names |
| Created by | Building a project — one project produces one assembly | A namespace declaration in a source file |
| Has a version | Yes, recorded in the manifest | No |
| Relationship | One assembly can contain many namespaces | One namespace can be spread across many assemblies |
| How you consume it | A project reference or a package reference | A using directive, or nothing if you write the full name |
| Affects what is deployed | Yes — every referenced assembly must be present at run time | No — it disappears into type names at compile time |
// File: Employees/Employee.cs
namespace Anvi.Employees.Core.Employees;
public sealed record Employee(int Id, string FullName, string Department);
// File: Employees/EmployeeValidator.cs
namespace Anvi.Employees.Core.Employees;
public sealed class EmployeeValidator
{
public bool IsValid(Employee employee) => employee.FullName.Length > 0;
}
// File: Leave/LeaveRequest.cs
namespace Anvi.Employees.Core.Leave;
public sealed record LeaveRequest(int EmployeeId, DateOnly From, DateOnly To);- Three files, two namespaces, one assembly. Building this project produces a single Anvi.Employees.Core.dll containing all of it.
- The namespace does not have to match the folder. Tooling and convention expect it to, and matching makes a codebase navigable, but the compiler does not care.
- The semicolon form declares a namespace for the whole file. The older braced form is equivalent and still common in existing code.
- A caller referencing this project writes using Anvi.Employees.Core.Employees; to reach Employee. That directive resolves a name. What makes the type available at all is the project reference.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Anvi.Employees.Api</AssemblyName>
<RootNamespace>Anvi.Employees.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Anvi.Employees.Core\Anvi.Employees.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Anvi.Employees.Core.Employees" />
</ItemGroup>
</Project>- AssemblyName sets the output file name. RootNamespace is only a default for new files your editor creates — it does not force anything on existing code.
- ProjectReference builds the other project and copies its assembly into this one's output folder. Remove it and no using directive in the world will make Employee resolve.
- The Using item adds a global using for that namespace across the whole project, so individual files do not repeat it. This is the same mechanism ImplicitUsings uses.
That leaves one everyday puzzle worth settling, because it is the clearest demonstration of the split. Console lives in the System namespace. Writing Console.WriteLine with no using System; at the top of the file looks like the compiler making an exception for a favourite type. It is not.
ImplicitUsings in the project file tells the SDK to generate a small file in the obj folder containing global using directives for a standard set of namespaces. A global using applies to every file in the project. For a plain console or library project that set includes System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading and System.Threading.Tasks. Web and worker projects get a longer list.
The generated file is real and readable. Look in obj for a file whose name ends with GlobalUsings.g.cs and you will see exactly which namespaces your project is getting for free. Set ImplicitUsings to disable and the file empties out, at which point Console needs using System; again and roughly half of your codebase stops compiling.
This is also the cleanest illustration of the split. Turning ImplicitUsings off changes nothing about which assemblies you reference or what ships in your output folder. It only changes which names can be written unqualified.
Summary
- An assembly is one compiled unit — intermediate language, metadata and a manifest — normally a .dll
- Namespaces organise type names and have no file, version or deployment presence
- One assembly holds many namespaces; one namespace can span several assemblies, as Microsoft.Extensions.Logging does
- A reference makes types available; a using directive only lets you write their names unqualified
- ImplicitUsings generates global using directives in obj, which is why Console resolves without using System
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Make the split visible
Create a class library with two namespaces in it, reference it from a console project, and build. Look in the console project's output folder and count the .dll files.
Now open the obj folder of the console project and find the generated global usings file. Which namespaces are you getting without asking?
Show solution
One .dll per project, regardless of how many namespaces each contains. The output folder shows two assemblies and says nothing at all about namespaces, because namespaces are not a deployment concept.
The generated file lists the implicit set for your project type. Reading it once removes a small, persistent mystery about why some types need a directive and others do not.
dotnet new classlib -o Anvi.Employees.Core
dotnet new console -o Anvi.Employees.Tool
cd Anvi.Employees.Tool
dotnet add reference ../Anvi.Employees.Core/Anvi.Employees.Core.csproj
dotnet buildThink about it
One assembly or three?
The employees API has grown to roughly 60 files: HTTP endpoints, domain types and validation, and data access. Someone proposes splitting it into three projects so that the domain cannot reference the HTTP layer.
What does the split actually enforce that folders do not, and what does it cost?
Show solution
Folders are a suggestion. Nothing stops a domain class in a single project from referencing an HTTP type, and under deadline pressure someone will. Project references are directional and checked by the compiler, so a domain project that does not reference the API project cannot use its types, full stop.
The costs are real: three project files to keep aligned, a slower build, and the standing temptation to add a reference back the wrong way to unblock something. A split you are not willing to defend in review will quietly become a split in name only.
A defensible middle position is to split only where you intend to enforce a rule, and leave the rest in one project until the rule matters.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.