1
零侵入;注入
零侵入 = 不动别人的代码
常规做法(侵入式):
你要加功能,得去改 Program.cs、App.xaml.cs
→ 主程序代码被你改了
零侵入:
你写一个独立的库,放个实现了 IHostingStartup 的类
→ 主程序一行不用动,自动发现并加载
注入 = 往里塞东西
csharp
// 在 Configure 里,你想加什么就加什么
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IMyService, MyService>(); // 注入新服务
});
}
对比
侵入式: 零侵入 + 注入:
主程序 App.cs 你的插件.cs
┌──────────────┐ ┌────────────────┐
│ 改这行 │ │ IHostingStartup │
│ 加那行 ←── 你插进去的 │ Configure() │
│ 删第三行 │ │ 往里塞服务 │
└──────────────┘ └────────────────┘
↓
自动被主程序发现并执行
零侵入 = 不碰原代码;注入 = 把自己的东西塞进容器。 两者配合,实现插件式扩展。
2
DBG
刚才所有日志前面的 [DBG] = Debug(调试级别)。
日志等级
[VRB] Verbose ─ 最啰嗦,什么都记
[DBG] Debug ─ 调试用,启动/初始化信息
[INF] Information ─ 常规信息,如 "Now listening on..."
[WRN] Warning ─ 警告,可能有问题
[ERR] Error ─ 出错了
[FTL] Fatal ─ 致命错误,程序要挂
这个项目里
注册用的是 Serilog,配置了按级别输出:
[DBG] ≈ 给开发人员看的,用户看不见
[INF] ≈ 关键节点信息,如启动成功
[ERR] ≈ 需要关注的错误
所以之前那些带 [DBG] 的日志都是调试级别,正常运行时可能就不会输出,只在调试模式下才记录。不影响功能,也不代表有问题。
3
2026-07-21 15:13:25.556 INF Hosting environment: Production
人话
"当前运行环境:生产模式"
为什么是 Production
ASP.NET Core 默认就是 Production,除非你显式设置了环境变量:
ASPNETCORE_ENVIRONMENT=Development → 开发模式(详细信息、开发者异常页)
ASPNETCORE_ENVIRONMENT=Staging → 预发布
(不设置) → 默认 Production
对项目的影响
| 模式 | 影响 |
|---|---|
| Development | 开发者异常页、详细错误信息 |
| Production(当前) | 不暴露内部错误详情、性能优化 |
跑在 Production 是合理的。
4
2026-07-21 15:13:25.556 INF Application started. Press Ctrl+C to shut down.
5
2026-07-21 15:13:25.891 DBG Creating DbConnection.
这是整个数据库操作的第一步------创建连接对象。
完整顺序回顾
15:13:25.891 Creating DbConnection ← 你这条(第1步:准备连接对象)
15:13:25.897 Created DbConnection (2ms) ← 连接对象创建完毕
15:13:25.903 Creating DbCommand ← 准备 SQL 命令
15:13:25.908 Initialized DbCommand (5ms) ← 命令就绪
15:13:25.917 Opening connection ← 真正打开连接
15:13:25.933 Opened connection ← 连上了
Create(创建) → Open(打开) → Execute(执行)
↑你在这里
创建连接对象 ≠ 打开连接 。就像买了根网线 ≠ 插上去通了。Creating DbConnection 是 new 了一个连接对象,16ms 后才真正 Opening connection 连数据库。
DbConnection
DbConnection = 跟数据库之间的那根线,EF Core 用它连 MySQL。
类比
DbConnection = 电话线
Creating DbConnection → 准备话机、扯好线
Opening connection → 拨号
Opened connection → 接通了,可以说话
Executing command → 说话(发 SQL)
Close / Dispose → 挂电话
代码里你不需要手动管
EF Core 和连接池自动处理:
csharp
await using var context = await _factory.CreateDbContextAsync();
// DbConnection 由 EF Core 内部创建和打开,你感知不到
var result = await context.Components.FindAsync(1);
// 内部:获取连接 → 打开 → 执行 SQL → 返回结果 → 连接归还连接池(不是关闭)
// await using 结束,DbContext 归还池
连接池
DbConnection 也不是用完就销毁的
┌──────────────────────┐
│ 连接池 (MySQL) │
│ [空闲] [空闲] [空闲] │ ← 一直保持打开,下次直接用
└──────────────────────┘
比每次都 new + open 快得多
DbContext 池(1024个)管理的是 C# 对象,DbConnection 的底层连接是 MySQL 的连接池在管,两个池各管各的。
6
2026-07-21 15:13:25.897 DBG Created DbConnection. (2ms).
连接对象创建完成,花了 2ms。
这 2ms 做了什么
new MySqlConnection("Server=localhost;Database=xray_v1;...")
↓
检查连接字符串格式、初始化连接池引用
↓
打印:Created DbConnection (2ms)
此时连接还没打开 (电话线准备好了但没拨号),16ms 后才 Opening connection。
891ms: Creating → 897ms: Created (2ms) → 917ms: Opening → 933ms: Opened
创建对象 创建完成 拨号 通了
就是确认一句:连接字符串没问题,对象 new 出来了。
7
2026-07-21 15:13:25.903 DBG Creating DbCommand for 'ExecuteScalar'.
连接对象好了,现在准备 SQL 命令。
线准备好了 → 准备要说的话
Creating DbCommand = 拼好这句 SQL:"查一下这些表在不在"
下一步 908ms:Initialized DbCommand(拼好了)。
ExecuteScalar
前面解释过,快速回顾:
执行一条 SQL,只返回一个值。
不是返回多行表数据 → 不是 ExecuteReader
不是插入修改删除 → 不是 ExecuteNonQuery
就是一个值 → ExecuteScalar
这里查的是:"表存在吗?" → 返回一个数字(4 代表有,0 代表没有)。
分开讲:
1. ExecuteScalar
是一种执行方式,不是 SQL 内容。
ExecuteScalar → 执行查询,只返回一个值(如:数字 4)
ExecuteReader → 执行查询,返回多行数据
ExecuteNonQuery → 执行更新,返回影响行数
2. 检查表是否存在的 SQL
是 查询的内容,EF Core 内部自动生成的,类似:
sql
-- EF Core 帮你写的,你不用管
SELECT 表数量 FROM MySQL系统表 WHERE 表名 IN (...)
3. EF Core 内部
指你看不到的 EF Core 源码里的逻辑。
拼起来
EF Core 内部生成 SQL: "查一下那 4 张表在不在" ← 自动的,你不用写
↓
包装成 DbCommand,标记为 'ExecuteScalar' 类型 ← 执行方式:返回一个值
↓
发给 MySQL ← 查到结果:在/不在
ExecuteScalar = 返回单个值的执行方式。检查表是否存在的 SQL = EF Core 自动生成的查询内容。两回事,一个是怎么执行,一个是在查什么。
8
2026-07-21 15:13:25.907 DBG Created DbCommand for 'ExecuteScalar' (1ms).
命令对象包好了,耗时 1ms。下一步是连接数据库执行。
对象=DbCommand for 'ExecuteScalar' =检查表是否存在的 SQL
对,理解正确。
DbCommand for 'ExecuteScalar'
│
├── 包裹的 SQL = "这 4 张表在不在?"
│
└── 执行方式 = ExecuteScalar(返回一个数字)
三者关系:
DbCommand 对象(信封)
└── 里面装着:检查表是否存在的 SQL(信的内容)
└── 执行类型标记:ExecuteScalar(只需要一个返回值)
9
2026-07-21 15:13:25.908 DBG Initialized DbCommand for 'ExecuteScalar' (5ms).
初始化完成,命令就绪可以执行。从创建 (903ms) 到初始化 (908ms) 共 5ms。
对。Created(创建好了)→ Initialized(就绪,随时可以发)→ Opening(开始连数据库)。Initialized 就是摩拳擦掌准备上场的那一步。
10
Parameters=\[\], CommandType='Text', CommandTimeout='30'
SELECT CASE WHEN COUNT(*) = 0 THEN FALSE ELSE TRUE END
FROM information_schema.tables
WHERE table_type = 'BASE TABLE' AND table_schema = '_v1'
这条日志显示的是 EF Core 执行 SQL 的完整命令信息:
[Parameters=[], CommandType='Text', CommandTimeout='30']
分解解释:
-
Parameters=\[\] --- 参数为空。这条 SQL 不需要外部传参,表名
'_v1'是直接写在 SQL 里的硬编码字符串。 -
CommandType='Text' --- 命令类型是文本,也就是一条原始 SQL 字符串。EF Core 直接把这串 SELECT 语句发给 MySQL,不走存储过程。
-
CommandTimeout='30' --- 超时时间 30 秒。如果这条查询 30 秒内没返回结果,就会抛超时异常。
下面的 SQL 就是上一轮讲过的:EnsureCreatedAsync 在检查 _v1 库里到底有没有表 。COUNT(*) = 0 → 没表就返回 TRUE(需要建表),有表返回 FALSE(啥也不干)。
简单说,就是 EF Core 启动时问 MySQL:"兄弟,_v1 库里有表吗?没有的话我帮你建。" 这条日志就是这次"询问"的完整记录。
EnsureCreatedAsync 是什么
这是 EF Core 提供的一个方法,一句话概括:
"库和表不存在就自动建,已经存在就啥也不干。"
它怎么跑的
AppDbContextExtensions.cs 注册了一个后台服务:
csharp
internal class EnsureDatabaseCreatedHostedService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var context = _dbContextFactory.CreateDbContext(); // ← 从池子拿一个 DbContext
await context.Database.EnsureCreatedAsync(stoppingToken); // ← 核心:自动建库建表
}
}
程序启动 → 这个服务自动运行 → 调用 EnsureCreatedAsync。
它干了什么(分步骤)
| 步骤 | 做什么 | 对应你看到的日志 |
|---|---|---|
| 1 | 打开 MySQL 连接 | Opening connection to '_v1' on 'localhost' |
| 2 | 查 information_schema.tables,看库里有没有表 |
SELECT ... FROM information_schema.tables WHERE table_schema = '_v1' |
| 3a | 有表 → 直接结束,啥也不做 | 日志就停了 |
| 3b | 没表 → 根据你的 AppDbContext 里定义的 4 个实体类,自动生成 CREATE TABLE 语句并执行 |
你会看到一堆 CREATE TABLE 日志 |
和另一个方法 MigrateAsync 的区别
EnsureCreatedAsync |
MigrateAsync |
|
|---|---|---|
| 适用场景 | 开发/小项目,不用迁移文件 | 生产环境,用迁移文件管理版本 |
| 建库建表 | ✅ 自动 | ✅ 通过迁移文件 |
| 表已存在时 | 啥也不干 | 执行未应用的迁移 |
| 表结构变了 | ❌ 不会更新,需要删库重建 | ✅ 通过新迁移文件增量更新 |
| 本项目用哪个 | ✅ 这个 | ❌ |
你们项目注释也写了:"自动建表(EnsureCreated 适用于无迁移文件的场景)"。
简单类比
就像你租了个空房子(MySQL 服务器),进门时:
- 先看看卧室厨房有没有家具(查
information_schema.tables) - 没有 → 帮你搬进来(
CREATE TABLE) - 已经有了 → 直接合租,不动你的东西
所以你日志里那条 SELECT 就是"看有没有家具"这一步。后续没看到 CREATE TABLE,说明数据库之前已经建好了。
这是筛选条件,限定只统计"用户真正创建的表":
拆解
WHERE table_type = 'BASE TABLE' ← 只要"实体表"
AND table_schema = '_v1' ← 只看 _v1 这个库
为什么加 table_type = 'BASE TABLE'?
MySQL 每个库里不只有你建的表,还有五花八门的东西:
| table_type | 是什么 | 举例 |
|---|---|---|
| BASE TABLE | 你建的实体表 | recipes、components |
| VIEW | 视图(虚拟表) | 查询结果伪装成的表 |
| SYSTEM VIEW | 系统视图 | information_schema 自己的表不算在内 |
如果不加这个过滤,COUNT(*) 可能把视图也算进去。
举个例子:假设你的库是这样的:
xray_v1/
├── recipes ← BASE TABLE(你建的)
├── components ← BASE TABLE(你建的)
├── my_view ← VIEW(视图,不算实体表)
- 不加过滤:
COUNT(*) = 3→TRUE - 加了过滤:
COUNT(*) = 2→TRUE
EF Core 要确认的是有没有实体表需要它管,视图不是它建的,它不管,所以要过滤掉。
table_schema = '_v1'
锁定只看 _v1 这一个库,别把其他库的表算进来。information_schema.tables 存的是整个 MySQL 服务器上所有库的所有表信息。
11
这是 X-Ray 设备上电启动的标准流程:
加载轴限位配置 → 读取运动轴的软/硬限位参数(轴能跑多远、别撞了)
通电 X-Ray 光管 → 给 X 射线源上电(核心成像部件,类似灯泡点亮)
扫图丢弃投影数 写入PLC → 告诉 PLC:扫描时开头丢掉几帧图像(去除不稳定帧)
进出板方向 写入PLC → 告诉 PLC:PCB 板从左进右出还是右进左出
Smema模式 写入PLC → 告诉 PLC:上下料通讯协议模式(SMEMA 是产线设备通讯标准)
正在连接复判站 Socket → 连到复判工位(检测完人工复核的那个工位)
整体流程
数据库 ✅ → 加载机械配置 → 点亮光管 → 发参数给 PLC → 连复判站
PLC 是设备的大脑,前面几步都在往 PLC 里写运行参数。光管、轴限位、进出板、SMEMA 这些都是实际物理硬件的初始化,说明程序已经从"软件准备"进入了"硬件就绪"阶段。
12
2026-07-21 15:13:27.471 INF SupXDriver 初始化,开始
2026-07-21 15:13:27.765 INF SupXDriver 搜索到1个设备,使用设备0
2026-07-21 15:13:27.767 INF SupXDriver 校准中,请稍后...
2026-07-21 15:13:33.260 INF SupXDriver 校准结束
2026-07-21 15:13:33.261 INF SupXDriver 初始化,成功
光管驱动初始化,一次教科书级别的硬件自检流程:
初始化,开始 → 驱动启动
搜索到1个设备,使用设备0 → 扫描到 1 个 X-Ray 探测器/光管,选第 0 个(第一个)
校准中,请稍后... → 自动校准(探测器跟光管对位、参数自整定)
校准结束 → 校准完成,花了约 5.5 秒
初始化,成功 → 驱动就绪
关键信息
- 搜索到 1 个设备 --- 说明硬件连接正常,驱动能找到探测器。如果这条日志说"搜索到 0 个设备",那就是硬件线没插或驱动没装。
- 校准 5.5 秒(27.767 → 33.260)--- 这是探测器在做自动校准,比如暗场校正、增益校准之类的,确保图像质量正常。
- 为什么硬件初始化比连数据库慢? 数据库操作是纯 CPU + 网络,毫秒级。硬件校准是真物理设备在跑,秒级,5.5 秒非常正常。
整体启动时序已经推进到:
数据库 ✅ → PLC 参数 ✅ → 光管通电 ✅ → 探测器校准 ✅
硬件层基本就绪,接下来就该加载业务模块了。
13
2026-07-21 15:13:33.264 INF 图像服务初始化完成,相机读取任务已启动
2026-07-21 15:13:33.266 INF APP 自动清理过期文件...
两步收尾动作:
图像服务初始化完成,相机读取任务已启动
探测器校准完,图像采集服务正式跑起来了。"相机读取任务已启动"说明后台起了一个持续循环任务,不断从探测器拿图像数据,有板子进来就采图。
[APP] 自动清理过期文件...
打扫卫生------把过期的日志、临时图片、缓存文件删掉,免得硬盘撑爆。
当前启动进度
数据库 ✅ → PLC 参数 ✅ → 光管通电 ✅ → 探测器校准 ✅ → 图像采集启动 ✅ → 清理临时文件...
到这里硬件和基础服务基本全部就绪,设备处于待机状态,就等 PCB 板进来触发检测流程了。