Modules and packages
C# programs are organised into assemblies — units of deployment, versioning, and security. Each assembly contains compiled CIL plus metadata; assemblies are the basis on which the CLR loads code at runtime. The conventional unit of source organisation is the namespace (covered in Namespaces); the conventional unit of project organisation is the project file (.csproj); the conventional unit of dependency management is the NuGet package. Together, the four levels — assembly, namespace, project, package — cover the C# build, deployment, and dependency story.
This page covers the compilation pipeline, the project-file model, NuGet package management, multi-targeting, and the conventions of modern .NET code organisation.
The compilation pipeline
A C# program is built in three principal stages:
- Compilation — the Roslyn compiler parses the source files, type-checks, and emits Common Intermediate Language (CIL) into an assembly file.
- Restoration — the build tooling restores the project’s dependencies (NuGet packages, project references) into the build cache.
- Execution — the CLR loads the assembly, resolves dependencies, and just-in-time compiles the CIL to native code as it executes.
The conventional command-line tool is dotnet:
dotnet new console -o myapp # create a new project
cd myapp
dotnet restore # restore dependencies
dotnet build # compile
dotnet run # build and run
dotnet publish # produce a deployable artifact
Each invocation runs the corresponding stage. The pipeline is implemented by MSBuild, the build system shared across .NET tooling.
Assemblies and metadata
An assembly is a .dll (library) or .exe (executable) file containing:
- CIL — the compiled program in an architecture-neutral instruction set.
- Metadata — type definitions, member signatures, references to other assemblies, attributes, custom data.
- Resources — embedded files (icons, strings, configuration data) declared in the project.
- Manifest — the assembly’s identity (name, version, culture, public-key token) and the list of files it comprises.
The CLR consumes the metadata to resolve types and members at load time; reflection (the System.Reflection API) exposes the metadata to user code.
using System.Reflection;
Assembly asm = typeof(SomeType).Assembly;
Type[] types = asm.GetTypes();
Version? ver = asm.GetName().Version;
foreach (var type in types) {
Console.WriteLine(type.FullName);
}
The reflection API is the principal mechanism for runtime type inspection; it powers serialisation, dependency injection, ORM mapping, and many other patterns.
Namespaces and assemblies
Namespaces and assemblies are independent. A namespace may span multiple assemblies (the standard library splits System across many assemblies), and an assembly may contain types in multiple namespaces (most user assemblies do). The decisions are independent:
- The namespace is a source-level organisation: it determines how callers refer to types.
- The assembly is a build-and-deployment unit: it determines what is shipped together.
// in my_lib.dll:
namespace MyCompany.Utilities {
public class Helper { /* ... */ }
}
namespace MyCompany.Internal {
internal class HelperImpl { /* ... */ }
}
The two namespaces are part of the same assembly. The internal access modifier limits visibility to within the assembly.
Project files (.csproj)
Each project is described by a .csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MyCompany.Utilities</RootNamespace>
<AssemblyName>MyCompany.Utilities</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>
The principal elements:
<OutputType>—Library(default),Exe,WinExe.<TargetFramework>— the .NET version (net8.0,net6.0,netstandard2.0).<Nullable>— controls the nullable annotation context.<ImplicitUsings>— adds a curated set ofusingdirectives.<RootNamespace>— the default namespace for new files.<PackageReference>— a NuGet dependency.<ProjectReference>— a sibling project dependency.
The SDK-style project file (the format above, with Sdk="Microsoft.NET.Sdk") is the conventional contemporary format. The older .csproj format had hundreds of lines listing every source file; the SDK-style format implicitly includes all .cs files in the directory.
A solution file (.sln) groups multiple projects and their build configurations; a typical .NET application has one solution containing several projects.
NuGet packages
NuGet is the .NET package manager. A package is a .nupkg file (a zip archive) containing:
- One or more compiled assemblies.
- A
nuspecmanifest describing the package. - Optional content files (configuration, scripts).
- Dependencies on other NuGet packages.
The package model is the conventional way to distribute .NET libraries. Adding a dependency is dotnet add package:
dotnet add package Newtonsoft.Json
dotnet add package Microsoft.Extensions.Logging --version 8.0.0
The command updates the .csproj file with a <PackageReference> and runs dotnet restore to download the package into the local cache.
The standard library itself is structured into NuGet packages; many .NET features (logging, dependency injection, configuration, EF Core, ASP.NET Core) are distributed as separate packages.
The principal concerns:
- Versioning —
<PackageReference Version="1.2.3" />specifies an exact version;Version="[1.2.3,)"admits any version ≥ 1.2.3. - Transitive dependencies — packages depend on other packages; NuGet resolves the graph.
- Source vs binary — packages may include source (loaded as compiler input) or binaries (linked as references). The conventional case is binary.
- Symbol packages —
.snupkgfiles containing debug symbols, distributed alongside the regular package.
Multi-targeting
A library may target multiple .NET versions through multi-targeting:
<PropertyGroup>
<TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
The build produces three sets of binaries — one per framework — and the consumer’s project picks the one matching its own target. The conventional discipline:
- Library projects multi-target to support the broadest set of consumers.
- Application projects single-target the version they ship against.
netstandard2.0is the lowest common denominator; it is supported by all .NET implementations from .NET Framework 4.6.1 onward.
Conditional compilation by target is admitted via predefined symbols:
#if NET8_0_OR_GREATER
// .NET 8 and later
#elif NET6_0
// .NET 6 only
#elif NETSTANDARD2_0
// .NET Standard 2.0
#endif
The mechanism is the conventional way to use newer APIs when available and fall back on older targets.
Static and shared libraries (assemblies and how they are loaded)
The CLR’s loading model differs from C/C++. Assemblies are not “static” or “shared” in the traditional sense; they are units that the runtime loads on demand:
- Application assembly — the
.exe(or.dllwith an entry point) that starts the program. - Referenced assemblies — the dependencies. The runtime loads them lazily when types from them are first used.
- Dynamically loaded assemblies — assemblies loaded via
Assembly.LoadFromorAssemblyLoadContext. The conventional pattern for plugin architectures.
Assembly resolution follows a precedence order: the application’s directory, NuGet cache, framework directories, system directories. The mechanism is configured per-application through the AppContext and runtimeconfig.json.
The dotnet publish command produces a self-contained deployment:
dotnet publish -c Release -r linux-x64 --self-contained
The output includes the application, all its dependencies, and the .NET runtime, ready for deployment without a separately-installed runtime. The contrast is with framework-dependent deployment, which requires the runtime to be present on the target machine.
Strong-naming and the GAC briefly
Older .NET Framework code used the Global Assembly Cache (GAC) — a system-wide store of strong-named assemblies. Strong-naming is signing an assembly with a public/private key pair, producing a unique assembly identity.
In .NET (the cross-platform successor to .NET Framework), the GAC and strong-naming are largely deprecated. The conventional contemporary deployment is per-application, with NuGet packages in the build output and no shared cache.
Strong-naming is still admitted for backward compatibility but not required for new code.
AppContext and runtime configuration
The AppContext class exposes configuration values affecting runtime behaviour:
AppContext.SetSwitch("System.Globalization.Invariant", true);
Switches admit per-application opt-ins to runtime behaviours. The conventional configuration:
runtimeconfig.json— generated alongside the.exe; specifies the target framework, switches, GC settings.appsettings.json— the conventional application-level configuration (read viaMicrosoft.Extensions.Configuration).- Environment variables —
DOTNET_*for runtime settings.
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Globalization.Invariant": true
}
}
}
The configuration mechanism is the conventional way to control runtime behaviour without recompilation.
Common patterns
Single project, multiple files
MyApp/
├── MyApp.csproj
├── Program.cs
├── Models/
│ ├── User.cs
│ └── Order.cs
├── Services/
│ ├── UserService.cs
│ └── OrderService.cs
└── Utilities/
└── DateExtensions.cs
Files are organised by feature or by layer; the namespace conventionally mirrors the folder structure. The <RootNamespace> property in the .csproj is the prefix for new files.
Solution with multiple projects
MyApp.sln
├── MyApp.Core/ (library: domain logic)
│ └── MyApp.Core.csproj
├── MyApp.Data/ (library: data access)
│ └── MyApp.Data.csproj
├── MyApp.Web/ (web application)
│ └── MyApp.Web.csproj
└── MyApp.Tests/ (test project)
└── MyApp.Tests.csproj
Each project is its own assembly; project references express dependencies. The conventional layered architecture — domain, data access, presentation, tests — admits clear separation and per-layer testing.
Internal-visible-to for tests
[assembly: InternalsVisibleTo("MyApp.Tests")]
The attribute extends internal access to the test assembly, admitting tests of internal types.
Source generators
C# 9 introduced source generators — code that runs at compile time and adds source files to the compilation:
[Generator]
public class MyGenerator : ISourceGenerator {
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context) {
var source = "/* generated code */";
context.AddSource("Generated.cs", source);
}
}
The mechanism is the conventional contemporary alternative to runtime reflection in many cases: serialisation, dependency injection, equality, AOT-friendly code paths. Source generators are configured as <Analyzer> in the project file or shipped via NuGet packages.
A note on the larger .NET ecosystem
The .NET ecosystem extends well beyond the BCL:
- ASP.NET Core — web frameworks (MVC, Razor Pages, Minimal APIs, Blazor).
- Entity Framework Core — ORM with LINQ-based queries.
- MAUI — cross-platform UI for desktop and mobile.
- WPF, Windows Forms — Windows desktop UI.
- gRPC — high-performance RPC.
- SignalR — real-time messaging.
- xUnit, NUnit, MSTest — testing frameworks.
- Serilog, NLog — logging.
- AutoMapper, Mapster — object mapping.
- Polly — resilience and transient-fault handling.
The conventional advice: start with the SDK-supplied templates (dotnet new); add NuGet packages as needed; use the BCL for fundamentals and the third-party libraries for higher-level concerns.