722:零侵入;DBG;

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 服务器),进门时:

  1. 先看看卧室厨房有没有家具(查 information_schema.tables
  2. 没有 → 帮你搬进来(CREATE TABLE
  3. 已经有了 → 直接合租,不动你的东西

所以你日志里那条 SELECT 就是"看有没有家具"这一步。后续没看到 CREATE TABLE,说明数据库之前已经建好了。

这是筛选条件,限定只统计"用户真正创建的表":


拆解

复制代码
WHERE table_type = 'BASE TABLE'      ← 只要"实体表"
  AND table_schema = '_v1'       ← 只看 _v1 这个库

为什么加 table_type = 'BASE TABLE'

MySQL 每个库里不只有你建的表,还有五花八门的东西:

table_type 是什么 举例
BASE TABLE 你建的实体表 recipescomponents
VIEW 视图(虚拟表) 查询结果伪装成的表
SYSTEM VIEW 系统视图 information_schema 自己的表不算在内

如果不加这个过滤,COUNT(*) 可能把视图也算进去。

举个例子:假设你的库是这样的:

复制代码
xray_v1/
├── recipes          ← BASE TABLE(你建的)
├── components       ← BASE TABLE(你建的)
├── my_view          ← VIEW(视图,不算实体表)
  • 不加过滤:COUNT(*) = 3TRUE
  • 加了过滤:COUNT(*) = 2TRUE

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 板进来触发检测流程了。

相关推荐
-银雾鸢尾-10 小时前
C#中的StringBuilder相关方法
开发语言·c#
-银雾鸢尾-10 小时前
C#中结构体与类的区别;抽象类与接口的区别
开发语言·c#
心平气和量大福大14 小时前
C#-WPF-Window主窗体
开发语言·c#·wpf
白露与泡影15 小时前
Arthas 实战指南:从方法耗时定位到 JVM 变量热修改
服务器·jvm·c#
EIP低代码平台19 小时前
EIP低代码平台系统-字典功能讲解
低代码·c#·工作流
吉普赛的歌20 小时前
YARP负载均衡配置了多个相同接口导致的报错
c#·yarp
张人玉1 天前
C# WinForms——工厂管理系统(C# WinForms)
数据库·sqlite·c#·winform
夜莺悠吟1 天前
关于对 C# 中 ImplicitUsings,GlobalUsings 的讨论
c#·.net
不正经学生1 天前
C 语言函数深入剖析(基础篇)—— 从零理解函数的每一个细节
c语言·开发语言·数据结构·算法·c#