.NET 10 WPF 技术栈最终决策¶
- 决策日期:2026-08-25
- 状态:已批准,作为当前唯一现行技术基线
- 适用产品:长期维护的 Windows 企业、工业、工具型或商业桌面应用
- 数据路线:已确定采用 ASP.NET Core 10 API + EF Core 10 + SQL Server(多用户)
- 决策来源:
docs/archive/2026-08-25中的框架比较、环境、视觉、工程闭环、效率及映射专题文档 - 变更规则:偏离“必选/默认”项或引入“明确不采用”项,必须提交 ADR,说明收益、成本、风险、验证结果和退出方案
一、最终结论¶
最终选择:
Windows 11 + Visual Studio 2026
.NET 10 LTS + C# 14 + WPF
.NET 10 内置 WPF Fluent Theme + 自有 Design Tokens
CommunityToolkit.Mvvm
Microsoft.Extensions.Hosting / DI / Configuration / Options / Logging
CQRS-lite 非对称映射:EF Projection + Mapperly(纯数据)+ 显式领域行为/ViewModel 构造
System.Text.Json + IHttpClientFactory + Microsoft.Extensions.Http.Resilience
ASP.NET Core 10 API + EF Core 10 + SQL Server(多用户)
MSTest + Microsoft.Testing.Platform
Git + Central Package Management + 锁定还原 + NuGet Audit
Windows CI/CD + 签名 + 分环发布 + 可观测性
Windows App SDK / CsWin32 / WebView2(仅按明确需求接入)
这是一套 Windows 专用、多用户客户端/服务端技术栈。当前没有 macOS/Linux 硬性需求,因此不采用 Avalonia UI;当前工程更重视企业控件、数据密集型 UI、部署灵活性和长期维护,因此不采用 WinUI 3 作为主 UI 框架。业务数据由 API 统一访问,WPF 客户端不直连 SQL Server;SQLite 不再作为主业务数据库,Azure SQL 也不在当前数据基线内。未来只有明确的离线缓存/同步需求并通过 ADR 后才能引入本地数据库。
二、最终决策总表¶
| 领域 | 最终选择 | 级别 | 备注 |
|---|---|---|---|
| 开发系统 | Windows 11 x64 | 必选 | 按产品要求增加 ARM64 验证 |
| IDE | Visual Studio 2026 | 必选 | 安装 .NET desktop development 与 ASP.NET/Web 工作负载 |
| SDK | 最新稳定版 .NET 10 LTS | 必选 | global.json 固定,allowPrerelease: false |
| 语言 | C# 14 + Nullable | 必选 | AnalysisLevel=latest-recommended |
| 客户端目标框架 | net10.0-windows |
必选 | Windows 专用 WPF 应用 |
| 服务端目标框架 | net10.0 |
必选 | ASP.NET Core API 不依赖 WPF/Windows UI 程序集 |
| UI | WPF | 必选 | 不迁移 WinUI 3,不为潜在跨平台切换 Avalonia |
| 基础主题 | .NET 10 内置 WPF Fluent Theme | 必选 | 不叠加另一套完整主题 |
| 设计系统 | 语义颜色、字体、4 epx 间距、圆角、图标、组件状态 | 必选 | 页面不散写颜色、字号和间距 |
| MVVM | CommunityToolkit.Mvvm | 必选 | 字段式 [ObservableProperty]、RelayCommand、ObservableValidator |
| 应用宿主 | Microsoft.Extensions.Hosting | 默认 | 中型及以上项目统一 DI、配置、日志和生命周期 |
| 映射 | CQRS-lite + Mapperly 源码生成 + 显式领域语义 | 必选规范 | 自动化纯数据转换;不映射聚合或 Observable/ViewModel 类型 |
| JSON | System.Text.Json | 默认 | 无明确原因不引入第二套 JSON 框架 |
| HTTP | IHttpClientFactory + Http.Resilience | 联网必选 | typed client、超时、有限重试、熔断、取消 |
| OpenAPI | ASP.NET Core OpenAPI + Kiota 或 NSwag 二选一 | 必选 | API 契约生成 Client/DTO,外包一层业务 Facade |
| 服务端 | ASP.NET Core 10 API | 必选 | 承担认证授权、业务用例、审计、并发和限流 |
| API 组织 | Minimal APIs + Route Groups | 默认 | Endpoint 保持薄;复杂 MVC 约定场景可由 ADR 改用 Controllers |
| 持久化 | EF Core 10 + SQL Server | 必选 | 服务端短生命周期 DbContext;客户端不得直连数据库 |
| 身份 | 独立身份 ADR;优先 OIDC/OAuth 2.0,Entra 场景使用 MSAL.NET + WAM | 待身份 ADR | 采用 OAuth 时桌面端是 Public Client,不保存 client secret |
| Windows 能力 | Windows App SDK Stable | 严格按需 | 先做最小 POC,再决定打包与 Runtime |
| Win32 | Microsoft.Windows.CsWin32 | 按需 | 优先于手写 P/Invoke |
| Web 内容 | WebView2 | 按需 | 只用于适合 Web 化的局部能力 |
| 日志 | ILogger;客户端 File/Async,服务端结构化日志 | 默认 | 轮转/采集、容量限制、关联、脱敏 |
| 遥测 | OpenTelemetry + Azure Monitor | 按合规需求 | 先定义告知、采样、地域和关闭方式 |
| 测试 | MSTest.Sdk + Microsoft.Testing.Platform | 必选 | 解决方案内不混用 VSTest 配置 |
| UI 自动化 | FlaUI/企业 UIA 工具 | 按风险 | 仅覆盖少量关键流程 |
| 可访问性 | Accessibility Insights + Narrator + UIA | 发布门禁 | 与主题、DPI、键盘矩阵一起验证 |
| 版本 | Nerdbank.GitVersioning | 正式项目默认 | 版本、日志、包和 commit 可追溯 |
| CI/CD | GitHub Actions 或 Azure Pipelines Windows Agent | 必选 | PR、Main、Release 流水线分离 |
| 包格式 | MSIX | 默认 | ClickOnce、WiX/MSI/EXE 按交付场景替换 |
| 签名 | Store 签名、Azure Artifact Signing 或受信 OV 证书 | 发布必选 | 凭据只存在受控签名环境 |
三、框架决策¶
采用 WPF¶
原因:
- 目标平台仅 Windows。
- 复杂表格、表单、报表、图表、文档、打印和设计器生态成熟。
- WPF Designer、Blend、Hot Reload、绑定诊断和第三方控件体系完整。
- 现代 .NET 继续支持 WPF,并提供内置 Fluent Theme。
- 可以原地接入 Windows App SDK、WinRT、CsWin32、WebView2 和 MSIX。
- 相比重写 WinUI 3,工程风险、培训成本和控件替换成本更低。
不采用 WinUI 3 作为主框架¶
WinUI 3 仍是新的 Windows 原生 UI 战略方向,但本项目不以它为主框架,因为当前收益不足以覆盖 XAML 差异、Designer 缺失、DataGrid/第三方控件验证、打包和迁移成本。只有全新消费级 Windows 11 产品、Fluent/触控为核心价值且全部关键能力 POC 通过时,才单独立项评估。
不采用 Avalonia UI¶
当前不存在 Windows、macOS、Linux 同时交付的硬性需求。为“未来也许跨平台”提前承担样式系统、控件差异、平台适配和多平台发布测试成本不划算。跨平台成为正式需求时,应重新做最复杂 DataGrid、第三方控件和 Windows API 页面 POC。
四、解决方案与依赖边界¶
采用客户端/服务端分离的解决方案:
Product/
├─ global.json
├─ Directory.Build.props
├─ Directory.Packages.props
├─ NuGet.config
├─ version.json
├─ Product.sln
├─ .config/
│ └─ dotnet-tools.json
├─ src/
│ ├─ client/
│ │ ├─ Product.Client.App/ # WPF 入口、Composition Root
│ │ ├─ Product.Client.Presentation/ # View、ViewModel、导航、UI 状态
│ │ ├─ Product.Client.Application/ # 客户端用例、端口、PageState/EditState
│ │ ├─ Product.Client.Infrastructure/ # typed API client、文件、非权威缓存
│ │ ├─ Product.Client.Platform.Windows/ # WinAppSDK、CsWin32、托盘、通知
│ │ ├─ Product.Client.Themes/ # 多应用共享主题时独立
│ │ └─ Product.Client.DesignLab/ # 设计令牌和组件的可执行目录
│ └─ server/
│ ├─ Product.Server.Api/ # HTTP 端点、认证授权、Composition Root
│ ├─ Product.Server.Application/ # 服务端用例、Command/Query、端口
│ ├─ Product.Server.Domain/ # 实体、值对象、领域规则
│ └─ Product.Server.Infrastructure/ # EF Core、SQL、外部服务、迁移
├─ contracts/
│ └─ openapi/ # 受版本控制的 OpenAPI 契约快照
├─ tests/
│ ├─ Product.Client.Tests/
│ ├─ Product.Server.Domain.Tests/
│ ├─ Product.Server.Application.Tests/
│ ├─ Product.Server.IntegrationTests/
│ ├─ Product.ContractTests/
│ ├─ Product.ThemeTests/
│ └─ Product.UiTests/ # 确有高价值流程时添加
├─ eng/
│ ├─ templates/
│ ├─ scripts/
│ └─ package/
└─ docs/
依赖方向:
Client.App / Presentation → Client.Application
Client.Infrastructure → Client.Application abstractions
Client.Platform.Windows → Client.Application abstractions
Client → 仅通过 HTTPS/OpenAPI 契约访问 Server
Server.Api → Server.Application
Server.Application → Server.Domain
Server.Infrastructure → Server.Application / Server.Domain
Server.Domain → 仅 .NET 基础能力
客户端不得引用服务端 Domain、Infrastructure 或 EF Core 项目;服务端不得引用 WPF 项目。OpenAPI 是跨进程契约,客户端生成代码封装在 Client.Infrastructure 后,不让生成类型扩散到 ViewModel。小型项目可以合并同一进程内的空项目,但客户端/服务端进程边界和依赖方向不得合并。
五、UI 与设计系统¶
基线¶
.NET 10 WPF Fluent Theme
→ 自有 Light/Dark 语义颜色
→ Typography / Spacing / Corner / Icon Tokens
→ Button / Input / DataGrid 等基础样式
→ SearchBar / EmptyState / ErrorPanel 等复合组件
→ 业务页面
规则:
- 主题只选 Fluent,不混用 Material、Metro 或多套第三方主题。
ThemeMode=System能满足项目策略时使用;若实验性诊断不可接受,则显式合并 Fluent 字典并自行管理主题切换。- 颜色按
Surface/Text/Border/Accent/Danger/Success等语义命名。 - 间距使用 4 epx 网格,常用
4/8/12/16/24/32/48。 - 字体以 Segoe UI Variable 为基线,控制在四至六个层级。
- 页面使用
Grid + Auto/* + Min/MaxWidth,避免大面积固定尺寸。 - 只改属性使用 Style;只有视觉树变化才重写 ControlTemplate。
- 模板必须覆盖 Normal、Hover、Pressed、Disabled、Focus、Validation 和 High Contrast。
- 公共组件进入业务页面前,必须在
Product.Client.DesignLab展示全部状态。
每个核心页面验证:
Light / Dark / High Contrast
100% / 125% / 150% / 200% DPI
最小窗口 / 最大化 / 多显示器
中文 / 英文 / 长文本
鼠标 / 键盘 / Tab / Narrator
Loading / Empty / Error / Validation / Offline
六、MVVM 和交互规范¶
采用 CommunityToolkit.Mvvm:
ObservableObject或ObservableValidator。- 字段式
[ObservableProperty] private ...,保留 backing field 的静默初始化能力。 [RelayCommand]、AsyncRelayCommand和CancellationToken。[NotifyCanExecuteChangedFor]等 Toolkit 能力按实际联动使用。- Messenger 只处理真正跨组件且无法用显式依赖表达的事件。
- ViewModel 不暴露
Brush、Visibility、Window、Dispatcher或MessageBox等 WPF 类型。 - 业务规则不放在 code-behind、Behavior、Converter 或 ControlTemplate。
- 简单纯 View 事件可以留在 code-behind;重复的视觉交互按需使用
Microsoft.Xaml.Behaviors.Wpf。
导航:少页面使用 ContentControl + DataTemplate;多模块才定义小型 INavigationService 和 IDialogService,不默认引入 Prism。
七、API 契约、CQRS 投影、编辑模型与领域命令¶
采用 CQRS-lite 非对称映射:读取链路为查询效率设计,写入链路为业务不变量设计,不再要求每个功能形成机械对称的“DTO → Domain → ViewModel → Domain → DTO”闭环。
查询链路¶
SQL Server
↓ EF Core AsNoTracking + Select
或经 SQL Server 集成测试批准的 Mapperly IQueryable Projection
API Response DTO
↓ OpenAPI / HTTPS
Generated Client DTO(仅限 Client.Infrastructure)
↓ Feature 级 Mapperly Mapper
PageState / FormSeed
↓ ViewModel 构造函数或小型 ViewModelFactory
ViewModel
- 查询直接投影所需列到 Response DTO,不为只读列表和详情无意义地加载完整聚合。
- 简单、稳定、可完全翻译的投影可以使用 Mapperly
IQueryable<T>Projection;复杂计算、授权过滤、Provider 特性、时区/金额语义使用显式Select。 - Mapperly IQueryable Projection 的对象工厂、Null、枚举、引用处理等能力受表达式树限制;每个生成投影必须在真实 SQL Server Provider 上验证结果与生成 SQL。
- OpenAPI 生成类型不越过
Client.Infrastructure。API Facade 将其映射为按页面/用例定义的不可变PageState或FormSeed,Presentation 不引用生成客户端。 - 不强制建立通用
Client ReadModel。只有跨页面复用、组合多个 API、本地缓存/同步或确有客户端业务语义时才定义独立 ReadModel。
写入链路¶
ViewModel / EditModel
↓ 显式 CreateSubmitState(不可变纯数据快照)
SubmitState
↓ Feature 级 Mapperly Mapper.ToRequest
API Request DTO
↓ HTTPS
API Endpoint
↓ Feature 级 Mapperly Mapper.ToCommand
Application Command
↓ Handler
Domain Aggregate 行为(仅用于存在业务不变量的用例)
↓ EF Core Unit of Work
Result / Response DTO
- 表单使用独立
EditModel : ObservableValidator,负责即时交互校验和脏状态;提交时由显式CreateSubmitState()读取已生成属性并创建不可变纯数据快照。Mapperly 从 SubmitState 生成 Request,不直接读取 CommunityToolkit.Mvvm 生成的 ViewModel/EditModel 成员。 - API Request 可以通过 Mapperly 转换为 Application Command,但不得自动映射成 Aggregate 或 EF Entity。
- Handler 从 Repository/EF Core 加载现有聚合并调用显式领域行为,例如
order.ChangeAddress(...);新聚合通过领域工厂创建,远程 DTO 不承担Restore职责。 - 只有真正存在状态转换、跨字段约束等不变量时才引入富领域聚合;简单字典、报表和 CRUD 不制造空洞的 Aggregate/Factory,可由 Application Query/Command 直接协调投影和持久化端口。
- 写入完成后返回最小 Result 或重新执行读取投影,不把 EF 跟踪实体直接序列化为响应。
Mapperly 使用边界¶
Mapperly 是当前基线中的编译期映射工具,只用于无副作用的结构转换:
// Product.Client.Infrastructure
[Mapper(
RequiredMappingStrategy = RequiredMappingStrategy.Target,
AutoUserMappings = false,
PreferParameterlessConstructors = false)]
internal static partial class OrderClientMapper
{
internal static partial OrderPageState ToPageState(OrderResponse source);
internal static partial UpdateOrderRequest ToRequest(OrderSubmitState source);
}
// Product.Server.Api
[Mapper(
RequiredMappingStrategy = RequiredMappingStrategy.Both,
AutoUserMappings = false,
PreferParameterlessConstructors = false)]
internal static partial class OrderApiMapper
{
internal static partial UpdateOrderCommand ToCommand(UpdateOrderRequest source);
}
- 每个 Feature 定义自己的静态强类型 Mapper;不提供全局
IMapper、运行时类型分派或跨功能“大一统”配置。 - 客户端 Mapper 与服务端 Mapper 分属各自进程项目,不建立同时引用两端实现程序集的共享 Mapper 项目。
- 等价模型转换默认使用
RequiredMappingStrategy.Both;允许源模型是目标模型超集的投影使用Target,被忽略成员必须显式记录。 AutoUserMappings=false;金额、币种、时区、标识、枚举兼容和 Null 语义等自定义转换必须通过明确命名的方法接入。- Mapperly 可以创建新的 DTO、Command、PageState、FormSeed 或纯数据 SubmitState。
ObservableObject、ObservableValidator和 ViewModel 不得作为 Mapperly 的源或目标,避免源码生成器链、setter 通知和生命周期副作用。 - Domain Aggregate/EF Entity 只能作为经批准的服务端只读 IQueryable Projection 源;不得作为普通对象映射源,更不得作为自动映射目标。
- 禁止使用 existing-target mapping 修补已绑定 ViewModel。刷新时生成新 PageState/FormSeed,再由 ViewModel 显式替换状态或创建新实例。
- 领域创建、状态转换、权限判断、冲突合并、默认值决策和任何有副作用的操作必须手写,不隐藏在生成 Mapper、setter、AfterMap 或对象工厂中。
- Mapperly 的未映射成员和不安全转换诊断提升为构建错误;生成器版本由 Central Package Management 固定,升级时审查诊断变化和生成代码差异。
映射与投影测试¶
- 结构映射测试断言金额、币种、时区、枚举、Null、ID、并发版本和嵌套集合,不只验证对象非空。
- 每个 IQueryable Projection 使用 SQL Server 集成测试验证可翻译性、筛选/分页顺序、结果和关键 SQL 形态;不能只用内存集合测试。
- ViewModel 测试验证初始化不会触发命令、验证弹窗或外部调用,并验证刷新不会覆盖用户尚未提交的编辑状态。
- OpenAPI 契约发生字段变化时,CI 必须同时通过客户端重新生成、Mapperly 编译诊断和关键字段 Contract 测试。
AutoMapper/Mapster 不进入当前基线;不得与 Mapperly 并存来解决同一类映射。确有 Mapperly 无法合理覆盖的场景时,优先手写局部转换;只有形成独立 ADR、替换边界和退出方案后才评估第二套 Mapper。
八、宿主、配置和生命周期¶
WPF 客户端和 ASP.NET Core API 分别拥有独立 Host 与 Composition Root。WPF 客户端生命周期:
Bootstrap logger
→ 读取最小配置
→ 构建 Host / DI
→ 验证 Options 和数据迁移
→ 创建并显示 MainWindow
→ 启动必要后台服务
→ 正常停止 Host 并 Flush 日志
App.xaml删除StartupUri,在组合根注册 Window、ViewModel、Application Service 和 Infrastructure。- 使用构造函数注入,不使用静态 Service Locator。
- 默认配置放
appsettings.json;用户配置、日志和缓存放%LocalAppData%\Company\Product。 - 配置文件带 schema/version,升级时显式迁移;重要文件使用临时文件加原子替换。
- 客户端不保存数据库管理员密码、API secret、云密钥或服务端秘密。
- 客户端启动不执行服务端数据库迁移;API 启动也不自动执行生产迁移。
ASP.NET Core API 启动时验证 Options、身份配置、数据库连接可用性和关键依赖;数据库迁移由部署流水线中的独立迁移步骤/受控迁移工具执行。迁移失败时停止发布,不启动一半新版本实例。
九、数据与服务通信¶
已选拓扑¶
WPF Client
→ HTTPS + 用户身份(优先 access token)
→ ASP.NET Core 10 API
→ Server.Application / Server.Domain
→ EF Core 10
→ SQL Server
认证、授权、审计、事务、并发、限流、数据库密钥和迁移属于服务端;桌面端不引用 EF Core、不携带数据库连接字符串。采用 OAuth/OIDC 时 WPF 是 Public Client,使用 Authorization Code + PKCE/WAM 等适合本机应用的流程,不保存 client secret。多用户不自动等于多租户:若产品需要租户隔离,必须另行确定租户标识、数据隔离模型、越权测试和运维边界。
API 与契约¶
- 只通过 HTTPS 暴露 API;生产环境启用 HSTS,并在入口层限制允许的主机、请求体大小和速率。
- API 采用资源/用例导向的 HTTP 契约,统一返回 RFC 7807
ProblemDetails错误体;不把异常消息或堆栈直接返回客户端。 - 默认使用 Minimal APIs + Route Groups 按 Feature 组织端点;Endpoint 只处理 HTTP、身份和 DTO 转换,业务规则进入 Application/Domain。一个 Feature 内不混用 Controllers 与 Minimal APIs。
- 除健康探测、登录回调等显式白名单外,端点默认要求认证;授权使用服务端 Policy/Scope/Role 和资源级检查,不能只依赖客户端隐藏按钮。
- OpenAPI 是客户端生成和契约测试的依据。选择 Kiota 或 NSwag 之一并固定版本;生成代码外包业务 Facade,ViewModel 不直接依赖生成客户端。
- 契约从
/api/v1起显式版本化;新增字段保持向后兼容,删除/改义字段先经过弃用窗口和客户端使用率验证。 ProblemDetails扩展包含稳定、可测试的业务错误码和 CorrelationId;时间使用明确时区的 ISO 8601/DateTimeOffset语义,金额同时传数值与币种,枚举新增值按前向兼容处理。- 列表接口必须分页并设置最大页大小;筛选和排序字段使用白名单,禁止把任意表达式透传到数据库。
- 写请求默认不自动重试;确需可重放的创建/提交操作时,由 API 定义幂等键、保存期限和冲突语义。
- 客户端贯穿
CancellationToken,服务端将HttpContext.RequestAborted传到应用层和 EF Core。
EF Core 与 SQL Server¶
DbContext按 API 请求/应用用例使用短生命周期,由服务端 DI 管理;不得注册为 Singleton,也不得跨线程共享。- EF Entity 和
DbContext只存在于服务端 Infrastructure;API 不直接返回 EF Entity,应用层不向客户端泄漏IQueryable。 - 查询默认使用投影;只读查询使用
AsNoTracking,修改聚合时在一个明确工作单元内完成。 - 事务边界以单个应用用例为默认;跨外部系统的一致性使用 Outbox/补偿等经过 ADR 的方案,不引入分布式事务作为默认。
- 可并发修改的记录使用
rowversion/并发令牌;API 将版本传给客户端,并用409 Conflict或412 Precondition Failed表达冲突,不做静默覆盖。 - Migration 纳入版本控制,并在 CI 生成 Migration Bundle 或经审查的 SQL Script 作为发布构件。生产发布采用先扩展、后迁移数据、再收缩的兼容策略;发布前备份并验证恢复,禁止应用启动时自动迁移生产库。
- API 数据库账号遵循最小权限,迁移账号与运行账号分离。部署环境支持时优先使用 Windows 集成身份/gMSA 等受控身份;否则从服务端密钥存储注入凭据,不把秘密写入仓库或客户端配置。
- 使用
Microsoft.EntityFrameworkCore.SqlServerProvider 和UseSqlServer;按实际网络与故障模型配置EnableRetryOnFailure,显式设置并核对 EF 与数据库的 SQL Server 兼容级别。 - SQL Server 版本、Edition、实例拓扑和高可用方案由部署 ADR 确定;所有生产查询、Migration、索引与故障恢复演练必须在相同版本/兼容级别的环境验证。
HTTP¶
- typed
HttpClient+ System.Text.Json。 - 每个请求有总超时,用户取消贯穿
CancellationToken。 - 只重试明确的瞬时错误;非幂等写请求不自动重试。
- 使用指数退避、jitter 和 circuit breaker。
- 记录 correlation ID、耗时和状态码,不记录 token 和敏感正文。
- 认证失败、授权失败、并发冲突、校验失败、限流和服务不可用映射为明确的客户端状态,不用异常文本驱动 UI。
- CI 重新生成 OpenAPI 客户端并检查差异,避免服务端契约变更未同步到桌面端。
十、Windows 平台能力¶
接入优先级:
- Windows App SDK 仅为明确的生命周期、窗口、通知、资源等能力引入。
- 先用最小 POC 验证 API、最低系统、打包模型和 Runtime 部署。
- 非打包应用可按官方要求配置
WindowsPackageType=None;MSIX 路线不得照搬。 - Win32 调用优先 CsWin32,不手写易错签名。
- WebView2 只用于已有 Web 模块、HTML 编辑器、报表和在线内容,不把普通 WPF 表单 Web 化。
十一、日志、安全与可观测性¶
- 业务代码只依赖
ILogger<T>;客户端默认使用 Serilog File + Async Provider,服务端输出结构化日志并由托管平台/采集器集中保存。 - 客户端文件日志按日期/大小轮转并限制保留天数和总容量;服务端日志由采集平台设置保留、容量和访问控制。
- 客户端记录版本、系统、架构和 SessionId;客户端生成/透传 CorrelationId,API 将其关联到请求、应用用例和数据库操作。
- 捕获 Dispatcher、AppDomain 和 TaskScheduler 异常入口;状态损坏时安全退出。
- 不记录密码、令牌、完整个人信息和敏感业务正文。
- 提供用户主动触发的脱敏诊断包。
- API 至少提供存活/就绪健康检查、结构化请求日志、错误率、延迟和依赖调用指标;健康检查不得返回连接字符串、异常堆栈等敏感细节。
- 安全审计日志与诊断日志分离,记录操作者、动作、目标、结果和服务端时间;禁止客户端自行声明审计身份,禁止记录令牌和敏感正文。
- 服务端秘密只进入受控配置源;开发使用 User Secrets,生产使用部署平台的密钥/托管身份能力,不提交到仓库或打入安装包。
- 跨客户端/API/数据库的遥测采用 OpenTelemetry;接入 Azure Monitor 前完成隐私、地域、采样、保留和关闭机制评审。
- 使用 MSAL.NET/Entra ID 时采用 Public Client + PKCE/系统浏览器/WAM,不保存 client secret 或明文 refresh token。
十二、测试和质量门禁¶
测试技术:
MSTest.Sdk + Microsoft.Testing.Platform
├─ Client Application/ViewModel 单元测试
├─ Server Domain/Application 单元测试
├─ ASP.NET Core API 集成测试
├─ SQL Server Testcontainers 数据库集成测试
├─ OpenAPI Contract、Mapperly 映射与兼容性测试
├─ Theme/ResourceDictionary STA 测试
├─ WireMock.Net(复杂 HTTP 场景)
└─ FlaUI/UIA(少量高价值 UI 流程)
- API 集成测试覆盖认证/授权、校验、
ProblemDetails、分页、限流、幂等和并发冲突;不得只测试 happy path。 - EF Core 仓储和 Migration 必须使用真实 SQL Server Provider 测试,不以 EF InMemory 或 SQLite 代替 SQL Server 语义。
- 本地与 CI 默认提供兼容 Testcontainers 的容器运行时;受控环境禁止容器时,改用每次测试独立创建并清理的真实 SQL Server 数据库,不能降低为内存 Provider。
- 每个 Migration 在空库升级和上一受支持版本升级两条路径验证;生产前在与目标 SQL Server 版本和兼容级别一致的环境执行。
- Contract 测试验证 OpenAPI 可生成、客户端可重新生成且无未提交差异,并检查破坏性契约变更。
- Mapperly 未映射成员/不安全转换诊断按规则提升为错误;CI 在重新生成 OpenAPI 客户端后重新编译全部 Feature Mapper。
- IQueryable Projection 测试使用真实 SQL Server Provider 验证翻译、结果、分页顺序和关键 SQL 形态,不允许退化为内存 LINQ。
- 时间逻辑注入
TimeProvider,测试使用Microsoft.Extensions.TimeProvider.Testing,不使用Thread.Sleep。 - WPF 资源或控件测试使用 MSTest
STATestMethod。 - UI 自动化只覆盖启动、登录、主导航、核心 happy path 和安装升级冒烟。
- 截图测试只作为少量高价值补充,不作为主要测试手段。
- 每个映射测试断言业务字段、金额、币种、时区、枚举、Null、ID、并发版本和嵌套集合;不只断言对象非空。
- PR 必须通过 restore、format、analyzer、Release build、tests 和依赖漏洞检查。
十三、工程效率¶
默认启用:
- XAML Hot Reload、Live Visual Tree、Live Property Explorer 和 XAML Binding Failures。
d:设计时数据,覆盖空、长文本、错误、离线和无权限状态。- Code Cleanup on Save +
.editorconfig。 Product.Client.DesignLab作为主题和组件的可执行文档。- 仓库级
.config/dotnet-tools.json,不依赖未固定的全局 CLI。 company-wpf-app、company-wpf-feature、company-wpf-control内部模板。- Nerdbank.GitVersioning 统一程序集、日志、安装包和 commit 版本。
- 大型解决方案才使用
.slnf,构建异常时生成受控 MSBuild binlog。
GitHub Copilot 为团队策略允许时的可选工具:通过仓库指令限制技术栈和质量门禁;输出始终是待审查候选,不允许自动合并、签名或发布,也不向未经批准的服务提交密钥、客户数据和敏感日志。
十四、依赖和包清单¶
客户端默认包¶
CommunityToolkit.Mvvm
Microsoft.Extensions.Hosting
Microsoft.Extensions.Http.Resilience
Serilog.Extensions.Hosting
Serilog.Sinks.File
Serilog.Sinks.Async
MSTest.Sdk # 测试项目
服务端与数据默认包¶
Microsoft.AspNetCore.OpenApi
Microsoft.Extensions.ApiDescription.Server # PrivateAssets/构建时 OpenAPI
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design # PrivateAssets/开发时
Microsoft.AspNetCore.Mvc.Testing # API 集成测试
Testcontainers.MsSql # SQL Server 集成测试
OpenAPI 客户端生成器选择 Kiota 或 NSwag 之一,并固定在仓库 Tool Manifest/构建配置中;不得同时维护两套生成器。
映射源码生成器¶
只在实际定义 Feature Mapper 的客户端/服务端项目中引用。使用最新稳定版并由 Central Package Management 固定;不使用 Next/Preview 通道。
按身份路线¶
Microsoft.AspNetCore.Authentication.JwtBearer # Bearer Token API 时
Microsoft.Identity.Client # 使用 Entra ID 时
Microsoft.Identity.Client.Broker # 使用 WAM 时
严格按需¶
Microsoft.Xaml.Behaviors.Wpf
Microsoft.WindowsAppSDK
Microsoft.Windows.CsWin32
Microsoft.Web.WebView2
Microsoft.Extensions.TimeProvider.Testing # 测试
WireMock.Net # 测试
OpenTelemetry.Extensions.Hosting
Azure.Monitor.OpenTelemetry.Exporter
内置能力不重复装包:System.Text.Json、TimeProvider 和 WPF Fluent Theme 来自 .NET 基线。所有 NuGet 版本由 Directory.Packages.props 管理;Microsoft.AspNetCore、Microsoft.Extensions 与 EF Core 使用与 .NET 10 对齐的最新稳定 10.0.x 补丁。
十五、CI/CD、签名和发布¶
Checkout
→ 安装 global.json 指定的稳定 SDK
→ dotnet tool restore
→ dotnet restore --locked-mode
→ dotnet format --verify-no-changes
→ 构建 Server 并生成 OpenAPI 与 WPF Client
→ 检查生成结果无未提交差异/破坏性变更
→ dotnet build -c Release --no-restore(包含 Mapperly 严格诊断)
→ dotnet test -c Release --no-build(包含映射与 Projection 测试)
→ 使用 SQL Server Testcontainer 验证 Migration 和数据集成测试
→ NuGet vulnerability audit
→ publish WPF win-x64 / win-arm64(按目标)
→ publish ASP.NET Core API 与独立数据库迁移构件
→ 生成构件清单/SBOM(按组织要求)
→ 签名并生成 MSIX/MSI
→ 上传不可变 Artifact
→ 在预生产环境执行迁移、部署 API、完成契约/数据库/API 冒烟
→ 干净机器安装、升级 WPF 并完成端到端冒烟
→ 先发布兼容的新 API,再发布 WPF 试点环
→ 监控后逐步扩大范围
发布路线:
- 桌面客户端不能保证同时升级,API 必须定义最小受支持客户端版本和兼容窗口;破坏性变更通过新 API 版本迁移,不就地改变旧语义。
- 数据库迁移遵循扩展/收缩策略。回滚优先回滚 API 或前向修复数据库,不默认执行可能丢数据的 Down Migration。
- 默认 MSIX:Store、Intune/Configuration Manager 或自有
.appinstaller。 - WPF 内网简单更新可选择 ClickOnce。
- 驱动、Windows Service、复杂注册和安装事务采用 WiX/MSI/EXE。
- EXE、DLL 和安装包按渠道签名;自签名只用于开发或企业已下发信任的环境。
- 保留上一可回退包、迁移策略、Release Notes、哈希和构建来源。
十六、明确不作为默认选择¶
| 技术/做法 | 最终决定 |
|---|---|
| WinUI 3 | 不作为本项目主 UI;新消费级产品另行 POC |
| Avalonia UI | 无跨平台硬需求,不采用 |
| WinForms | 不用于当前复杂、长期演进 UI |
| .NET Framework 4.8 | 新项目不采用 |
| UWP | 不采用 |
| Prism | 默认不引入;真正插件化/Region 导航再评估 |
| ReactiveUI / DynamicData | 默认不引入;复杂响应式数据流出现后再评估 |
| MediatR 式进程内总线 | 默认不引入;跨切面管线价值明确时再评估 |
| Mapperly 普通对象映射 Domain Aggregate/EF Entity,或映射 Observable/ViewModel 类型 | 禁止;Domain/EF 只允许作为受测 IQueryable Projection 源,Observable/ViewModel 完全隔离 |
| Mapperly existing-target 更新已绑定对象 | 禁止;刷新时创建新状态并由 ViewModel 显式应用 |
| AutoMapper/Mapster 作为第二套通用 Mapper | 不进入基线;局部手写优先,例外必须提交 ADR |
全局通用 IMapper |
不采用,使用 Feature 级强类型方法 |
| WPF 客户端直连 SQL Server | 禁止;所有业务数据访问通过 API |
| SQLite 作为主业务数据库 | 不采用;仅允许经 ADR 批准的离线缓存/同步用途 |
| Azure SQL | 当前数据基线不采用;未来切换必须提交数据库部署 ADR 并完成兼容性、成本和运维验证 |
| 生产环境由 API 启动时自动迁移数据库 | 禁止;使用部署流水线中的受控迁移构件 |
| EF InMemory/SQLite 替代 SQL Server 集成测试 | 不采用;Provider 行为必须用 SQL Server 验证 |
| 完整第三方主题库 | 不默认引入,不与内置 Fluent Theme 混用 |
| 多套企业控件库 | 不采用;必要时只选一套并加薄适配层 |
| 大量截图测试 | 不采用;只保留少量高价值视觉基线 |
| Preview/Experimental 生产依赖 | 未经 ADR 不允许 |
| 全局未固定 CLI | 不允许,改用本地 Tool Manifest |
| AI 自动合并/发布 | 不允许 |
十七、按需技术的准入门槛¶
新增 Windows App SDK、WebView2、第二套 Mapper、商业控件、远程遥测、离线同步或 UI 自动化前,必须回答:
- 哪个已确认的需求无法由当前基线合理完成?
- 最小 POC 是否覆盖最难页面、最低系统、DPI、主题、可访问性和部署?
- 许可证、安全、隐私、包体、启动、升级和退出成本是什么?
- CI、干净机和离线环境如何验证?
- 如果依赖停止维护或不兼容,替换边界在哪里?
没有可验证答案,就不进入基线。
十八、实施顺序¶
1. 安装 VS 2026 和最新稳定 .NET 10 SDK
2. 提交 global.json / Directory.Build.props / Directory.Packages.props
3. 创建 Client 与 Server 分离的最小项目结构和依赖边界测试
4. 客户端接入 CommunityToolkit.Mvvm/Generic Host,服务端建立 ASP.NET Core Host
5. 完成身份、API 托管、SQL Server 版本/Edition/拓扑和租户模型 ADR
6. 建立 EF Core SQL Server Provider、第一条 Migration 和 SQL Server 集成测试
7. 实现第一条非对称链路:Query Projection 与 Command → Domain → EF Core
8. 发布 OpenAPI,选定 Kiota 或 NSwag;接入 Mapperly 严格诊断和 Feature Mapper
9. 生成客户端并封装 typed API Facade,完成 PageState/EditModel → SubmitState → API → Database 的端到端功能
10. 建立 Fluent Theme、Tokens 和 Product.Client.DesignLab
11. 建立认证授权、日志审计、异常处理、TimeProvider、健康检查和分层测试
12. 接入契约检查、数据库迁移验证、CI、版本化、依赖审计和 Release build
13. 部署 API/数据库预生产环境;确定 MSIX/ClickOnce/MSI 并完成签名、升级和端到端冒烟
14. 建立试点发布、诊断包、崩溃/API/更新监控和反馈闭环
15. 只按已发生的需求加入离线同步、Windows App SDK、WebView2、商业控件等可选项
十九、尚需由产品需求确定的分支¶
数据主路线已经确定:WPF → ASP.NET Core 10 API → EF Core 10 → SQL Server,不再保留 SQLite/Azure SQL 分支。以下不是该路线悬而未决,而是必须由部署和业务事实继续确定:
- 数据库部署:SQL Server 的版本、Edition、物理机/虚拟机/容器实例、单机/故障转移群集/Always On 拓扑,以及容量、备份、恢复、HA、RTO/RPO 和网络边界。
- API 托管:IIS/Windows Service、容器平台,还是 Azure App Service;如何滚动发布、扩缩容和回退。
- 身份:企业 Windows 集成身份、标准 OIDC/OAuth 2.0,还是 Entra ID/MSAL;授权策略和账号生命周期由谁管理。
- 租户:单组织多用户,还是多租户 SaaS;若为多租户,采用哪种数据隔离和租户解析方案。
- 离线:保持在线优先,还是确有本地缓存/离线编辑/同步需求;后者必须单独设计冲突、加密、清理和恢复策略。
- 发布:Store/MSIX、企业 MSIX、ClickOnce,还是 WiX/MSI/EXE。
- 架构:仅 x64,还是同时交付 ARM64。
- 运营:仅本地日志,还是合规后的远程遥测。
- 控件:内置控件足够,还是需要一套商业 Grid/Chart/Report 产品。
身份、租户、API 托管和数据库部署应在第一条生产纵向功能前形成 ADR;其余选择最迟在对应 POC 或发布准备前确定。
二十、交付闭环¶
需求与验收标准
→ 架构/隐私/部署 ADR
→ 纵向功能与 DesignLab
→ 单元/集成/主题/可访问性测试
→ PR 与 CI 门禁
→ 版本、签名、打包
→ 试点环发布
→ 启动、崩溃、API、更新监控
→ 用户反馈和支持工单
→ 回归测试、模板、规则或文档
→ 下一轮需求
线上问题不能只修代码;应至少回写为自动化测试、Analyzer/EditorConfig 规则、DesignLab 状态、项目模板、API 契约检查、ADR 或运行手册之一。
关键依据¶
- Microsoft:.NET 10
- Microsoft:WPF
- Microsoft:WPF Fluent Theme
- Microsoft:CommunityToolkit.Mvvm
- Microsoft:在 WPF 中使用 Generic Host
- Microsoft:Windows App SDK
- Microsoft:EF Core 10
- Microsoft:ASP.NET Core 10
- Microsoft:ASP.NET Core OpenAPI
- Microsoft:ASP.NET Core 身份认证
- Microsoft:ASP.NET Core API 错误处理
- Microsoft:EF Core SQL Server Provider
- Microsoft:EF Core Tracking 与查询投影
- Microsoft:EF Core 并发冲突
- Microsoft:EF Core 生产迁移
- Microsoft:CommunityToolkit.Mvvm ObservableValidator
- Mapperly:介绍与生成模型
- Mapperly:安装与生产依赖配置
- Mapperly:RequiredMappingStrategy
- Mapperly:IQueryable Projection 与限制
- Microsoft:HTTP Resilience
- Microsoft:MSTest 与 Microsoft.Testing.Platform
- Microsoft:Windows 打包
- Microsoft:可访问性测试