722:零侵入;DBG;
1
零侵入;注入
零侵入 = 不动别人的代码
常规做法(侵入式): 你要加功能,得去改 Program.cs、App.xaml.cs → 主程序代码被你改了 零侵入: 你写一个独立的库,放个实现了 IHostingStartup 的类 → 主程序一行不用动,自动发现并加载注入 = 往里塞东西
// 在 Configure 里,你想加什么就加什么publicvoidConfigure(IWebHostBuilderbuilder){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 和连接池自动处理:
awaitusingvarcontext=await_factory.CreateDbContextAsync();// DbConnection 由 EF Core 内部创建和打开,你感知不到varresult=awaitcontext.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 内部自动生成的,类似:
-- EF Core 帮你写的,你不用管SELECT表数量FROMMySQL系统表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注册了一个后台服务:
internalclassEnsureDatabaseCreatedHostedService:BackgroundService{protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){usingvarcontext=_dbContextFactory.CreateDbContext();// ← 从池子拿一个 DbContextawaitcontext.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 板进来触发检测流程了。
