浏览知识库目录

C#

项目、程序集、命名空间与依赖

理解命名空间、项目、程序集和 NuGet 包,通过项目引用建立单向依赖。

项目、程序集、命名空间与依赖

当控制台程序开始承担领域、数据库、网络和展示职责时,继续向同一个项目追加文件会让依赖方向失控。C# 的命名空间组织名称,项目定义构建边界,程序集是编译产物,NuGet 包则负责跨仓库分发;四者不能混为一谈。


一、学习目标

  • 区分命名空间、项目、程序集、解决方案和包
  • 使用项目引用建立编译期依赖
  • 通过 publicinternal 控制 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 定义:

  • StudyTaskTaskStatus
  • ITaskRepository
  • IRemoteTaskClient
  • TaskService

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

这会扩大兼容负担。优先测试公开行为,必要时只开放最小内部接口。


十一、练习与自测

练习:

  1. 建立四项目解决方案并画出项目引用图。
  2. 将一个 SQLite 类型误放进 Core,观察依赖污染,再用接口反转。
  3. 启用中央包管理和锁文件,验证 --locked-mode
  4. dotnet list package --include-transitive 区分直接与传递依赖。

自测:

  • 命名空间与程序集边界有何不同?
  • 为什么同仓库项目应使用 ProjectReference 而非复制 DLL?
  • Core 应在哪里声明数据库仓储接口?
  • 包锁文件能解决什么,又不能解决什么?

十二、官方资料

上一篇:控制流、方法与参数设计 | 下一篇:类、接口、记录与对象建模