C#
项目、程序集、命名空间与依赖
理解命名空间、项目、程序集和 NuGet 包,通过项目引用建立单向依赖。
发布于 2026年7月23日
项目、程序集、命名空间与依赖
当控制台程序开始承担领域、数据库、网络和展示职责时,继续向同一个项目追加文件会让依赖方向失控。C# 的命名空间组织名称,项目定义构建边界,程序集是编译产物,NuGet 包则负责跨仓库分发;四者不能混为一谈。
一、学习目标
- 区分命名空间、项目、程序集、解决方案和包
- 使用项目引用建立编译期依赖
- 通过
public与internal控制 API 表面 - 理解 NuGet 直接依赖和传递依赖
- 统一包版本与还原策略
- 建立
StudyTasks的单向依赖结构
二、五个层次
- 命名空间:避免类型名称冲突,不产生运行隔离
- 项目文件:定义源码、目标框架、引用和构建属性
- 程序集:通常是构建得到的
.dll或.exe - 解决方案:把多个项目交给工具统一管理
- NuGet 包:包含程序集、元数据和可能的构建资产
命名空间与目录常保持一致,但编译器不强制。一个项目可以包含多个命名空间,一个命名空间也可能分布在多个程序集。
namespace StudyTasks.Core.Models;
public sealed record StudyTask(int Id, string Title);
文件范围命名空间减少缩进,适合一份文件主要属于一个命名空间的情况。
三、项目文件
StudyTasks.Core.csproj 可以保持很小:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
SDK 默认包含项目目录下的 *.cs,不需要逐文件登记。obj 保存中间状态与还原图,bin 保存构建输出。
项目引用:
dotnet add src/StudyTasks.Infrastructure \
reference src/StudyTasks.Core
对应项目文件中的:
<ItemGroup>
<ProjectReference Include="..\StudyTasks.Core\StudyTasks.Core.csproj" />
</ItemGroup>
项目引用让构建系统知道顺序,并直接引用目标项目的输出。不要把同一解决方案内的 DLL 手工复制到 lib 目录。
四、依赖方向
本系列采用:
Cli -> Core
Cli -> Infrastructure
Infrastructure -> Core
Tests -> Core + Infrastructure
Core 定义:
StudyTask、TaskStatusITaskRepositoryIRemoteTaskClientTaskService
Infrastructure 实现 SQLite 与 HTTP 适配器;Cli 解析参数、组装依赖并显示结果。
如果 Core 为了记录日志而引用 Cli,或为了保存任务而直接引用 SQLite 包,依赖就反转了。正确做法是在核心层定义所需能力的接口,由外层实现。
五、程序集 API 表面
默认让实现细节保持 internal:
namespace StudyTasks.Infrastructure.Sqlite;
internal static class SqlStatements
{
internal const string SelectAll = """
SELECT id, title, status
FROM task
ORDER BY id;
""";
}
只有其他程序集确实需要使用的类型和成员才标记为 public。公开 API 一旦被其他项目依赖,改名、移动和更改签名都需要考虑兼容性。
测试应优先通过公开接口验证行为。若确有必要访问少量内部成员,可以使用 InternalsVisibleTo,但不要把它当作测试私有实现的默认手段。
六、添加 NuGet 依赖
dotnet add src/StudyTasks.Cli \
package System.CommandLine --version 2.0.10
dotnet add src/StudyTasks.Cli \
package Microsoft.Extensions.Hosting --version 10.0.10
dotnet add src/StudyTasks.Infrastructure \
package Microsoft.Data.Sqlite --version 10.0.10
包引用进入项目文件,dotnet restore 解析完整依赖图并写入资产文件。只声明代码直接使用的包,不依赖“某个上层包刚好传递带来”的类型。
查看依赖:
dotnet list package
dotnet list package --include-transitive
dotnet nuget list source
包名相似不代表来源相同。安装前核对所有者、仓库、版本、许可证和已知漏洞,不从陌生源复制命令。
七、集中管理版本
多项目可以使用 Directory.Packages.props:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="System.CommandLine" Version="2.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.10" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>
各项目只写:
<PackageReference Include="Microsoft.Data.Sqlite" />
集中版本避免同一解决方案出现无意差异。升级仍应通过分支完成,并运行完整构建、测试与发布烟测。
八、还原与可重复性
dotnet build 会隐式还原,但 CI 常显式分开:
dotnet restore --locked-mode
dotnet build --no-restore -c Release
dotnet test --no-build -c Release
应用项目可以启用锁文件:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
首次还原生成 packages.lock.json,应提交版本库。--locked-mode 在依赖图与锁文件不一致时失败,防止 CI 静默选择另一版本。
锁文件不能替代可信包源、签名策略和漏洞管理,也不能保证包本身安全。
九、解决方案构建
dotnet sln StudyTasks.slnx list
dotnet build StudyTasks.slnx -c Release
dotnet test StudyTasks.slnx -c Release --no-build
解决方案方便本地和 CI 统一操作,但项目引用才真正定义依赖图。即使某项目没有加入解决方案,只要被 ProjectReference 引用,构建仍可能包含它。
发布时指定入口项目,而不是整个解决方案:
dotnet publish src/StudyTasks.Cli -c Release
十、常见错误
用命名空间假装分层
同一程序集中的 Core 命名空间仍可直接访问内部实现。需要真正编译边界时拆项目。
双向项目引用
MSBuild 会拒绝循环。更重要的是,循环说明职责或抽象位置有问题。把共同契约下沉到 Core。
直接使用传递依赖
上层包移除该依赖后项目会突然失败。直接使用的包应直接声明。
为了测试把所有类型改成 public
这会扩大兼容负担。优先测试公开行为,必要时只开放最小内部接口。
十一、练习与自测
练习:
- 建立四项目解决方案并画出项目引用图。
- 将一个 SQLite 类型误放进 Core,观察依赖污染,再用接口反转。
- 启用中央包管理和锁文件,验证
--locked-mode。 - 用
dotnet list package --include-transitive区分直接与传递依赖。
自测:
- 命名空间与程序集边界有何不同?
- 为什么同仓库项目应使用 ProjectReference 而非复制 DLL?
- Core 应在哪里声明数据库仓储接口?
- 包锁文件能解决什么,又不能解决什么?
十二、官方资料
上一篇:控制流、方法与参数设计 | 下一篇:类、接口、记录与对象建模