Skip to Content
Week 05Day 1 - 解决方案结构

Day 1 - 解决方案结构

建议用时:240-300 分钟

你将学会什么

  • Solution、Project、Folder 分别是什么
  • 为什么项目变大后不能把所有代码放进一个 Program.cs
  • Core、Infrastructure、Entry 层分别放什么
  • 项目引用方向为什么不能乱
  • 如何用 dotnet 命令创建一个多项目解决方案
  • 如何把业务规则从入口代码里拆出去

本页先用控制台项目当入口层。以后换成桌面界面或 Web 接口时,分层思想不变:入口负责接收操作,Core 负责业务规则,Infrastructure 负责文件、数据库、网络等外部细节。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“第四周复盘与综合练习”,今天继续学习“解决方案结构”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“解决方案结构”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。解决方案结构不是为了好看,而是为了项目变大以后仍然能找得到、改得动、测得住。

1. Solution 是什么

Solution 是解决方案。

它本质上是一个管理多个项目的清单文件,后缀是:

.sln

一个解决方案可以包含:

  1. 控制台项目。
  2. 桌面项目。
  3. Web 项目。
  4. 类库项目。
  5. 测试项目。

2. Project 是什么

Project 是项目。

一个项目有自己的 .csproj 文件。

项目负责告诉 .NET:

  1. 这是控制台程序还是类库。
  2. 用什么 .NET 版本。
  3. 引用了哪些包。
  4. 引用了哪些其他项目。
  5. 编译哪些 .cs 文件。

3. Folder 是什么

Folder 只是文件夹。

文件夹可以帮助整理文件,但它不是一个独立编译单元。

区别:

名称作用
Solution管理多个项目
Project一个可编译单元
Folder整理文件

4. 为什么要分层

如果所有代码都在入口里,会出现这些问题:

  1. 输入、业务规则、文件保存混在一起。
  2. 业务规则不好测试。
  3. 换成桌面或 Web 时要重写大量代码。
  4. 文件保存改成数据库保存时容易影响业务逻辑。
  5. 代码越写越长,定位问题越来越慢。

分层的目标是:

不同职责放到不同位置。

5. Core 层放什么

Core 放最核心、最不应该依赖外部框架的东西。

常见内容:

内容示例
实体ProductOrderCustomer
接口IProductRepository
服务ProductService
业务规则名称不能为空、价格必须大于 0
枚举OrderStatus

Core 不应该关心:

  1. 控制台怎么输入。
  2. 桌面按钮怎么点击。
  3. Web 路由怎么写。
  4. 文件怎么保存。
  5. 数据库连接字符串是什么。

6. Infrastructure 层放什么

Infrastructure 放外部细节。

常见内容:

内容示例
文件保存FileProductRepository
数据库保存SqlProductRepository
网络请求HttpProductClient
日志实现文件日志、控制台日志
第三方 SDK支付、短信、邮件

Infrastructure 可以引用 Core,因为它要实现 Core 里定义的接口。

7. Entry 层放什么

Entry 是程序入口层。

本页用控制台项目表示入口层。以后它也可以换成桌面项目或 Web 项目。

入口层负责:

  1. 接收用户操作。
  2. 准备依赖对象。
  3. 调用 Core 服务。
  4. 把结果显示出来。

入口层不应该塞满业务规则。

8. Tests 层放什么

Tests 放测试。

测试优先测 Core,因为 Core 里是业务规则。

例如:

  1. 商品名为空时应该失败。
  2. 价格为 0 时应该失败。
  3. 正常商品能保存。
  4. 订单未提交不能支付。

测试项目通常引用 Core。

9. 项目引用方向

引用方向一定要清楚:

入口层可以引用 Core。 Infrastructure 可以引用 Core。 入口层可以引用 Infrastructure。 Core 不要引用入口层。 Core 不要引用 Infrastructure。

原因是:Core 要保持干净,才能复用和测试。

如果 Core 依赖了界面或数据库实现,后面换界面、换数据库都会很难。

本页常用命令速查

需求命令作用
创建解决方案dotnet new sln -n MyApp创建 .sln
创建类库项目dotnet new classlib -n MyApp.Core放核心业务代码
创建控制台项目dotnet new console -n MyApp.Cli放启动入口
创建测试项目dotnet new xunit -n MyApp.Tests放自动测试
项目加入解决方案dotnet sln add 项目.csproj让解决方案管理项目
添加项目引用dotnet add A.csproj reference B.csprojA 可以使用 B 的代码
还原依赖dotnet restore下载 NuGet 包
构建全部项目dotnet build检查编译
运行指定项目dotnet run --project 项目.csproj启动入口项目
测试全部项目dotnet test执行测试

常见结构:

MyApp.sln src/MyApp.Core src/MyApp.Cli tests/MyApp.Tests

判断项目引用方向:

Cli 引用 Core Tests 引用 Core Core 不引用 Cli Core 不引用 Tests

第二部分:把知识组合成完整例子

这一部分先看结果。今天最终要能看懂这种结构,并知道每一层负责什么。

先看效果:ProductApp 解决方案

ProductApp/ ProductApp.sln src/ ProductApp.Console/ 程序入口,接收输入并调用服务 ProductApp.Core/ 业务模型、接口、业务规则 ProductApp.Infrastructure/ 文件、数据库、网络等实现 tests/ ProductApp.Tests/ 测试业务规则和服务逻辑

引用方向应该是:

ProductApp.Console -> ProductApp.Core ProductApp.Console -> ProductApp.Infrastructure ProductApp.Infrastructure -> ProductApp.Core ProductApp.Core -> 不引用入口层,也不引用基础设施层

最重要的是:

Core 不应该依赖 Console、桌面界面、Web 接口、数据库实现。

Core 是业务中心。入口和外部实现都围着它转。

第三部分:跟着敲代码

从这里开始动手。先创建结构,再用单文件完整例子理解各层职责。

动手前先做这 3 件事

  1. 找一个空文件夹练习,不要直接在重要项目里乱建。
  2. 每执行一组命令就用 dotnet build 检查一次。
  3. 先理解引用方向,再写代码。

例子 1:创建解决方案和项目

dotnet new sln -n ProductApp --format sln mkdir -p src tests dotnet new console -n ProductApp.Console -o src/ProductApp.Console dotnet new classlib -n ProductApp.Core -o src/ProductApp.Core dotnet new classlib -n ProductApp.Infrastructure -o src/ProductApp.Infrastructure

这会创建一个解决方案和三个项目。

这里明确写 --format sln,是为了生成传统的 ProductApp.sln 文件。新版本 .NET SDK 默认可能生成 .slnx,文件名不同会导致后面的 dotnet sln ProductApp.sln ... 找不到文件。

例子 2:把项目加入解决方案

dotnet sln ProductApp.sln add src/ProductApp.Console/ProductApp.Console.csproj dotnet sln ProductApp.sln add src/ProductApp.Core/ProductApp.Core.csproj dotnet sln ProductApp.sln add src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj

加入解决方案后,ProductApp.sln 才知道这些项目属于同一个解决方案。

例子 3:添加项目引用

dotnet add src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj reference src/ProductApp.Core/ProductApp.Core.csproj dotnet add src/ProductApp.Console/ProductApp.Console.csproj reference src/ProductApp.Core/ProductApp.Core.csproj dotnet add src/ProductApp.Console/ProductApp.Console.csproj reference src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj

引用方向是:

Infrastructure -> Core Console -> Core Console -> Infrastructure

例子 4:构建整个解决方案

dotnet build ProductApp.sln

只要引用方向没错,项目应该能正常构建。

例子 5:用一个完整程序模拟三层结构

下面这个例子可以直接放进一个控制台项目的 Program.cs 运行。它把 Core、Infrastructure、Entry 都写在一个文件里,是为了先理解职责分离。

IProductRepository repository = new FileProductRepository("products.txt"); ProductService service = new ProductService(repository); try { Product product = service.Create(" 键盘 ", 199m); Console.WriteLine($"创建成功: {product.Name} | {product.Price}"); } catch (ArgumentException ex) { Console.WriteLine($"创建失败: {ex.Message}"); } class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } } interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class FileProductRepository : IProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { string line = $"{product.Name},{product.Price}"; File.AppendAllText(_path, line + Environment.NewLine); } }

这个单文件例子里:

代码实际应该放哪层
ProductCore
IProductRepositoryCore
ProductServiceCore
FileProductRepositoryInfrastructure
最上面的创建和调用代码Console / Desktop / Web 入口层

例子 6:Core 层代码应该长什么样

ProductService service = new ProductService(new MemoryProductRepository()); Product product = service.Create("鼠标", 88m); Console.WriteLine($"{product.Name} | {product.Price}"); class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } } interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class MemoryProductRepository : IProductRepository { public List<Product> Products { get; } = new List<Product>(); public void Save(Product product) { Products.Add(product); } }

这里用 MemoryProductRepository 是为了演示:Core 服务依赖接口,不依赖具体文件保存。

例子 7:错误结构是什么样

下面是错误方向,不要这样设计:

ProductApp.Core -> ProductApp.Console ProductApp.Core -> ProductApp.Infrastructure ProductService 直接 new FileProductRepository() ProductService 直接 Console.WriteLine()

问题是:业务层被入口和文件实现绑死,后面不好换,也不好测试。

例子 8:正确调用关系

入口层创建具体实现: FileProductRepository repository = new FileProductRepository("products.txt") 入口层把实现交给 Core 服务: ProductService service = new ProductService(repository) Core 服务只知道接口: IProductRepository

这就是依赖从外面传入的基础。下一页会专门讲依赖注入。

常见错误和修法

错误为什么错修法
所有代码放一个项目层次不清,测试困难拆成 App、Core、Tests 等项目
项目引用方向反了Core 依赖 UI 会导致复用困难UI 引用 Core,测试引用 Core
解决方案里没加项目IDE 或命令不好统一管理dotnet sln add 加入 .sln
命名空间混乱类型难找,引用容易错按项目和文件夹统一命名
不跑整体构建单项目能过不代表整体能过在解决方案根目录跑 dotnet build

小白重复敲写训练

工程结构先用命令创建,再亲手看目录变化。

训练 1:创建解决方案和两个项目

dotnet new sln -n Shop dotnet new console -n Shop.App dotnet new classlib -n Shop.Core dotnet sln Shop.sln add Shop.App/Shop.App.csproj dotnet sln Shop.sln add Shop.Core/Shop.Core.csproj

每执行一行就查看目录,不要一次全贴进去。

训练 2:添加项目引用

dotnet add Shop.App/Shop.App.csproj reference Shop.Core/Shop.Core.csproj dotnet list Shop.App/Shop.App.csproj reference

改动任务:再创建 Shop.Infrastructure 并加入解决方案。

训练 3:跨项目调用

// Shop.Core/PriceCalculator.cs namespace Shop.Core; public static class PriceCalculator { public static decimal Total(decimal price, int count) => price * count; }
// Shop.App/Program.cs using Shop.Core; Console.WriteLine(PriceCalculator.Total(20m, 3));

运行 dotnet run --project Shop.App 验证引用生效。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:解决方案结构。今天至少要掌握的 3 个点是:

1. Solution、Project、Folder 分别是什么 2. 为什么项目变大后不能把所有代码放进一个 `Program.cs` 3. Core、Infrastructure、Entry 层分别放什么

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:Solution、Project、Folder 分别是什么。

上位机专项练习

真实上位机不能把界面、通信、数据和配置全写在一个文件。先练习分层和项目引用。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:Core 层定义设备模型

namespace Hmi.Core; public class Device { public string Name { get; set; } = ""; public bool IsOnline { get; set; } }

运行结果或界面效果:

Core 层只描述设备,不关心界面

改动任务: 增加 IpAddress 属性。

专项例子 2:Infrastructure 层实现读取

using Hmi.Core; namespace Hmi.Infrastructure; public class FakeDeviceReader { public Device Read() => new() { Name = "PLC-01", IsOnline = true }; }

运行结果或界面效果:

Infrastructure 层负责外部设备或文件

改动任务: 把模拟设备名改成 PLC-02。

专项例子 3:App 层组合并显示

var reader = new Hmi.Infrastructure.FakeDeviceReader(); Hmi.Core.Device device = reader.Read(); Console.WriteLine($"{device.Name}: {(device.IsOnline ? "在线" : "离线")}");

运行结果或界面效果:

PLC-01: 在线

改动任务: 把 IsOnline 模拟为 false。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。建议先照着目录和命令做一遍,再用单文件代码理解职责。

作业 1:画出项目结构

要求:

  1. 有入口项目。
  2. 有 Core 项目。
  3. 有 Infrastructure 项目。
  4. 有 Tests 项目。
  5. 写清每层职责。

完整答案

ProductApp/ ProductApp.sln src/ ProductApp.Console/ 入口层:接收输入、调用服务、显示结果 ProductApp.Core/ 核心层:实体、接口、业务规则、服务 ProductApp.Infrastructure/ 基础设施层:文件、数据库、网络实现 tests/ ProductApp.Tests/ 测试层:验证 Core 的业务规则

作业 2:写出引用方向

完整答案

ProductApp.Console -> ProductApp.Core ProductApp.Console -> ProductApp.Infrastructure ProductApp.Infrastructure -> ProductApp.Core ProductApp.Tests -> ProductApp.Core ProductApp.Core 不引用 ProductApp.Console ProductApp.Core 不引用 ProductApp.Infrastructure

作业 3:创建解决方案命令

完整答案

dotnet new sln -n ProductApp --format sln mkdir -p src tests dotnet new console -n ProductApp.Console -o src/ProductApp.Console dotnet new classlib -n ProductApp.Core -o src/ProductApp.Core dotnet new classlib -n ProductApp.Infrastructure -o src/ProductApp.Infrastructure dotnet sln ProductApp.sln add src/ProductApp.Console/ProductApp.Console.csproj dotnet sln ProductApp.sln add src/ProductApp.Core/ProductApp.Core.csproj dotnet sln ProductApp.sln add src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj dotnet add src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj reference src/ProductApp.Core/ProductApp.Core.csproj dotnet add src/ProductApp.Console/ProductApp.Console.csproj reference src/ProductApp.Core/ProductApp.Core.csproj dotnet add src/ProductApp.Console/ProductApp.Console.csproj reference src/ProductApp.Infrastructure/ProductApp.Infrastructure.csproj dotnet build ProductApp.sln

作业 4:分层代码完整答案

IProductRepository repository = new FileProductRepository("products.txt"); ProductService service = new ProductService(repository); Product product = service.Create("显示器", 1299m); Console.WriteLine($"保存商品: {product.Name} | {product.Price}"); class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } } interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class FileProductRepository : IProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { File.AppendAllText(_path, $"{product.Name},{product.Price}" + Environment.NewLine); } }

作业验收

完成后检查:

  1. 能说清 Solution、Project、Folder 的区别。
  2. 能说清 Core、Infrastructure、Entry 分别放什么。
  3. 能说清为什么 Core 不依赖入口层。
  4. 能用命令创建基础解决方案结构。
  5. 能把商品创建规则放进 Core,而不是入口代码。

本页最后要记住

  1. Solution 管多个 Project。
  2. Project 是可编译单元,Folder 只是文件夹。
  3. Core 放业务规则和接口。
  4. Infrastructure 放文件、数据库、网络等实现。
  5. Entry 层负责接收操作并调用服务。
  6. Core 要保持干净,不依赖入口层和基础设施实现。
  7. 引用方向错了,项目后面会越来越难改。