跳转至

.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:

  • ObservableObjectObservableValidator
  • 字段式 [ObservableProperty] private ...,保留 backing field 的静默初始化能力。
  • [RelayCommand]AsyncRelayCommandCancellationToken
  • [NotifyCanExecuteChangedFor] 等 Toolkit 能力按实际联动使用。
  • Messenger 只处理真正跨组件且无法用显式依赖表达的事件。
  • ViewModel 不暴露 BrushVisibilityWindowDispatcherMessageBox 等 WPF 类型。
  • 业务规则不放在 code-behind、Behavior、Converter 或 ControlTemplate。
  • 简单纯 View 事件可以留在 code-behind;重复的视觉交互按需使用 Microsoft.Xaml.Behaviors.Wpf

导航:少页面使用 ContentControl + DataTemplate;多模块才定义小型 INavigationServiceIDialogService,不默认引入 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 将其映射为按页面/用例定义的不可变 PageStateFormSeed,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。ObservableObjectObservableValidator 和 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 Conflict412 Precondition Failed 表达冲突,不做静默覆盖。
  • Migration 纳入版本控制,并在 CI 生成 Migration Bundle 或经审查的 SQL Script 作为发布构件。生产发布采用先扩展、后迁移数据、再收缩的兼容策略;发布前备份并验证恢复,禁止应用启动时自动迁移生产库。
  • API 数据库账号遵循最小权限,迁移账号与运行账号分离。部署环境支持时优先使用 Windows 集成身份/gMSA 等受控身份;否则从服务端密钥存储注入凭据,不把秘密写入仓库或客户端配置。
  • 使用 Microsoft.EntityFrameworkCore.SqlServer Provider 和 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 平台能力

接入优先级:

.NET / WPF 已有 API
→ WinRT
→ Windows App SDK Stable
→ CsWin32
→ 自定义原生组件
  • 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-appcompany-wpf-featurecompany-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/构建配置中;不得同时维护两套生成器。

映射源码生成器

Riok.Mapperly                         # ExcludeAssets=runtime; PrivateAssets=all

只在实际定义 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 自动化前,必须回答:

  1. 哪个已确认的需求无法由当前基线合理完成?
  2. 最小 POC 是否覆盖最难页面、最低系统、DPI、主题、可访问性和部署?
  3. 许可证、安全、隐私、包体、启动、升级和退出成本是什么?
  4. CI、干净机和离线环境如何验证?
  5. 如果依赖停止维护或不兼容,替换边界在哪里?

没有可验证答案,就不进入基线。

十八、实施顺序

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 或运行手册之一。

关键依据