Skip to main content
ANVISoftware Solutions
Lesson 3 of 12Intermediate16 min

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:

 AssemblyNamespace
What it isA compiled file, normally a .dllA prefix that groups type names
Created byBuilding a project — one project produces one assemblyA namespace declaration in a source file
Has a versionYes, recorded in the manifestNo
RelationshipOne assembly can contain many namespacesOne namespace can be spread across many assemblies
How you consume itA project reference or a package referenceA using directive, or nothing if you write the full name
Affects what is deployedYes — every referenced assembly must be present at run timeNo — it disappears into type names at compile time
Anvi.Employees.Core — one assembly, three namespaces
C#
// 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.
Anvi.Employees.Api.csproj — the reference is what carries the dependency
XML
<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.

Shell
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 build

Think 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.

Which statement about assemblies and namespaces is correct?
You set ImplicitUsings to disable in a project that was using it. What breaks?

Saved in this browser only.