简介:本资源是一套基于.NET Framework 4.8的WinForm桌面应用完整工程,面向C#初学者与中级开发者,聚焦SQLite轻量级数据库与EntityFramework 6 ORM框架的实战集成。项目实现数据增删查功能,主界面通过ListView展示SQLite数据表内容,并封装EF分层调用逻辑;含EF暖机优化(GetItemCollection预加载)、App.config连接字符串配置、SQLite Expert兼容的sqlite.db3数据库文件(位于bin/Debug),以及NuGet依赖说明(System.Data.SQLite.Core、EF6等)。资源共183个文件,涵盖13个核心C#源码、40个运行时DLL、20个MSBuild transform文件、14个targets构建脚本及6个可执行exe,总大小35.51MB,结构规范,便于理解EF在WinForm中的落地流程。目前已有599人学习下载,提供开箱即用的数据库操作范例、可调试的完整解决方案及典型ORM工程组织方式,是掌握桌面端SQLite+EF开发模式的优质实践素材。
1. WinForm + SQLite + EntityFramework:为什么小工具、工业看板、本地数据采集系统都选它?
你手头有个温湿度监控面板,要离线记录传感器数据;或者在车间里写个设备点检表单,不连服务器、不装 SQL Server,但又不想手写INSERT INTO ... VALUES (...)拼 SQL 字符串;再或者,客户只要一个双击即用的.exe,部署时不能要求他先装数据库服务、配连接字符串、开防火墙端口——这时候,WinForm + SQLite + EntityFramework 就不是“能用”,而是“最稳的一条路”。
这不是玩具组合。SQLite 是嵌入式数据库的事实标准:单文件、零配置、ACID 完整、跨平台(.NET 6+ 已原生支持)、文件级加密可用(SQLCipher 兼容方案成熟);WinForm 虽老但极轻量,启动快、资源省、UI 控件稳定(尤其是DataGridView、ListView、Chart配合本地数据源毫无压力);而 EntityFramework Core(注意:不是 EF6)作为当前 .NET 生态主力 ORM,对 SQLite 的支持已打磨多年——它不只帮你省掉 SQL 拼接,更关键的是:自动管理连接生命周期、防 SQL 注入、支持 LINQ 查询翻译、提供变更跟踪与批量保存、可无缝切换到 SQL Server 做后期升级。
适合谁?不是做 SaaS 的团队,而是:产线工程师写的设备日志工具、质检员用的抽检记录本、实验室仪器数据归档器、离线版巡检 App、教育类课程设计作业、甚至嵌入式 HMI 的上位机配套软件。它们共性明确:单机运行、数据量中等(百万行内)、强依赖本地文件可靠性、开发周期紧、部署必须“复制即用”。本文就带你从零搭起这个组合,不绕弯、不跳坑,每一步命令可粘贴、每个配置有依据、每个报错有解法。
2. 环境准备与项目初始化:用 dotnet CLI 创建最小可行骨架
2.1 创建 WinForm 项目并启用 .NET 6+ SDK
WinForm 在 .NET 5+ 已完全现代化,不再依赖 Windows Desktop Runtime 单独安装(只要目标机有 .NET 6/7/8 Runtime 即可)。我们直接用 CLI 创建带窗体的项目:
dotnet new winforms -f net8.0 -n LocalDataTool cd LocalDataTool提示:
-f net8.0明确指定框架版本。EF Core 对 SQLite 的完整支持(如DateTimeOffset映射、JSON列类型)在 .NET 6+ 才稳定,避免用netcoreapp3.1或net5.0。
此时项目是纯 WinForm,无数据库能力。下一步引入 SQLite 和 EF Core。
2.2 安装必需 NuGet 包:精简到 3 个核心包
打开LocalDataTool.csproj,添加以下<PackageReference>(或用 CLI 一次性安装):
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 8.0.8 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.8 dotnet add package System.Data.SQLite.Core --version 1.0.118说明:
Microsoft.EntityFrameworkCore.Sqlite:EF Core 的 SQLite 提供程序,含查询翻译、迁移引擎、连接池。Microsoft.EntityFrameworkCore.Tools:提供dotnet ef命令(如dotnet ef migrations add Init),必须安装才能用迁移功能。System.Data.SQLite.Core:原生 SQLite ADO.NET 驱动,EF Core 底层依赖它。注意选Core版(跨平台),而非x86/x64限定版。版本1.0.118是目前与 .NET 8 兼容性最稳的(1.0.119在某些 ARM64 设备上有加载失败问题,见 GitHub #4217)。
安装后执行dotnet restore确保包下载完成。此时项目结构干净,无任何数据库代码,正适合我们从模型开始构建。
2.3 设计第一个实体:以温湿度记录为例建模
在项目根目录新建文件夹Models,创建SensorReading.cs:
using System; using System.ComponentModel.DataAnnotations; namespace LocalDataTool.Models { public class SensorReading { public int Id { get; set; } [Required] public string SensorId { get; set; } = string.Empty; // 传感器编号,如 "TEMP-001" public double Temperature { get; set; } public double Humidity { get; set; } public DateTime Timestamp { get; set; } = DateTime.Now; public string Location { get; set; } = "Unknown"; // 记录位置,便于分组查询 } }关键点说明:
Id为int主键,SQLite 默认使用INTEGER PRIMARY KEY,自动成为 rowid,性能最优。Timestamp设默认值DateTime.Now,避免插入时漏填;EF Core 会将其映射为TEXT类型(ISO8601 格式),这是 SQLite 推荐做法(比DATETIME类型更兼容)。[Required]触发 EF Core 的非空约束,生成迁移时会加NOT NULL。- 不用
DateTimeOffset:虽然 EF Core 支持,但 SQLite 原生无该类型,需额外序列化,增加复杂度;本地系统时间统一即可,无需时区处理。
这个类就是你的“业务语言”,后续所有数据库操作都围绕它展开——不用管表名、字段名、索引,EF Core 会按约定生成。
3. 构建 DbContext:连接字符串、上下文注册与生命周期管理
3.1 编写 SQLiteDbContext:专注数据访问契约
在Models文件夹下新建SQLiteDbContext.cs:
using Microsoft.EntityFrameworkCore; using LocalDataTool.Models; namespace LocalDataTool.Models { public class SQLiteDbContext : DbContext { public DbSet<SensorReading> SensorReadings { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) { // 关键:使用相对路径,确保 .exe 运行时数据库文件在同目录 var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sensor_data.db"); options.UseSqlite($"Data Source={dbPath};Cache=Shared;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置主键、索引、约束 modelBuilder.Entity<SensorReading>() .HasKey(e => e.Id); modelBuilder.Entity<SensorReading>() .HasIndex(e => e.SensorId); // 加速按传感器查询 modelBuilder.Entity<SensorReading>() .HasIndex(e => e.Timestamp); // 加速时间范围查询 modelBuilder.Entity<SensorReading>() .Property(e => e.Timestamp) .HasColumnType("TEXT"); // 显式指定存储为 TEXT,避免 EF Core 自动转 DATETIME } } }逻辑说明:
DbSet<SensorReading>声明了数据集入口,后续用context.SensorReadings.Add(...)即可。OnConfiguring中Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ...)是唯一可靠路径写法。BaseDirectory指向.exe所在目录(如C:\MyApp\LocalDataTool.exe→ 数据库存C:\MyApp\sensor_data.db),无论用户从哪启动、是否用快捷方式、是否打包成单文件,都指向正确位置。绝对不要用Application.StartupPath(WinForm 专用,且在某些部署场景下不可靠)或硬编码路径。Cache=Shared参数启用共享缓存模式,允许多个连接同时读写(EF Core 默认开启,但显式写出更清晰)。OnModelCreating中HasColumnType("TEXT")强制时间存为字符串,避免 SQLite 的DATETIME类型在不同文化环境下解析歧义(如2024-05-20 14:30:00vs20/05/2024 14:30:00)。
3.2 在 Program.cs 中注册 DbContext 并启用依赖注入
.NET 6+ 的 WinForm 启动模板已集成 DI 容器。修改Program.cs:
using LocalDataTool.Models; using Microsoft.EntityFrameworkCore; var builder = ApplicationConfiguration.CreateBuilder(args); // 注册 DbContext,作用域生命周期(每次请求新建,WinForm 中即每次窗体操作) builder.Services.AddDbContext<SQLiteDbContext>(options => options.UseSqlite($"Data Source={Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sensor_data.db")};Cache=Shared;")); // 可选:注册窗体为服务,便于依赖注入传 DbContext builder.Services.AddTransient<MainForm>(); var app = builder.Build(); app.Run();注意:
AddDbContext默认是 Scoped 生命周期,完全适配 WinForm 场景——每个窗体实例(如MainForm)可注入独立SQLiteDbContext实例,互不干扰。不需要Singleton(全局单例易引发并发冲突)或Transient(每次 new 太重)。
3.3 验证 DbContext 是否就绪:用 Package Manager Console 快速测试
打开 Visual Studio 的Package Manager Console(确保默认项目为LocalDataTool),执行:
dotnet ef migrations add Init dotnet ef database update预期结果:
migrations add Init生成Migrations/xxx_Init.cs,内容包含CREATE TABLE "SensorReadings"语句。database update执行迁移,在LocalDataTool.exe同目录生成sensor_data.db文件(约 12KB)。- 用 DB Browser for SQLite 打开该文件,可见
SensorReadings表,字段与SensorReading类一致,含Id,SensorId,Temperature,Humidity,Timestamp,Location,且Id为主键,SensorId和Timestamp有索引。
这步成功,证明 ORM 层已打通。数据库文件已就位,接下来就是 UI 绑定和业务逻辑。
4. WinForm 界面绑定与 CRUD 实现:从 DataGridView 到实时刷新
4.1 设计 MainForm:拖放控件 + 代码后台分离
打开MainForm.cs [Design],从工具箱拖入:
DataGridView(命名为dgReadings):显示所有记录Button(命名为btnAdd,Text="新增")Button(命名为btnRefresh,Text="刷新")StatusStrip(命名为statusStrip1,加ToolStripStatusLabel显示记录数)
双击按钮进入代码页,先声明私有字段:
private SQLiteDbContext _context; private BindingSource _bindingSource = new BindingSource();在MainForm构造函数中注入 DbContext 并初始化绑定:
public MainForm(SQLiteDbContext context) { InitializeComponent(); _context = context; // 初始化 BindingSource,绑定到 DbSet _bindingSource.DataSource = _context.SensorReadings.Local.ToBindingList(); // DataGridView 绑定到 BindingSource dgReadings.DataSource = _bindingSource; // 自动列宽、禁止用户排序(避免干扰本地排序逻辑) dgReadings.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); foreach (DataGridViewColumn col in dgReadings.Columns) col.SortMode = DataGridViewColumnSortMode.NotSortable; }关键点:
_context.SensorReadings.Local.ToBindingList()是 EF Core 提供的本地内存集合视图,它自动同步DbSet的新增/修改/删除状态。BindingSource绑定它,DataGridView就能实时响应数据变化,无需手动Refresh()。Local属性只返回已加载到内存的实体(首次为空),所以首次需手动加载数据(见 4.2)。
4.2 实现数据加载与新增:LINQ 查询 + 实体添加
在MainForm中添加LoadData()方法:
private async void LoadData() { try { // 清空本地集合,重新从数据库加载(避免脏数据) _context.SensorReadings.Local.Clear(); // 异步加载全部记录(.NET 8 支持异步 ToListAsync) var readings = await _context.SensorReadings .OrderByDescending(r => r.Timestamp) .ToListAsync(); // 将查询结果加入本地集合,BindingSource 自动更新 UI foreach (var reading in readings) { _context.SensorReadings.Local.Add(reading); } statusStrip1.Items[0].Text = $"共 {readings.Count} 条记录"; } catch (Exception ex) { MessageBox.Show($"加载失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } }在btnRefresh_Click中调用:
private void btnRefresh_Click(object sender, EventArgs e) => LoadData();在btnAdd_Click中实现新增:
private void btnAdd_Click(object sender, EventArgs e) { var newReading = new SensorReading { SensorId = $"TEMP-{DateTime.Now:HHmmss}", Temperature = 25.5 + new Random().NextDouble() * 5, Humidity = 45.0 + new Random().NextDouble() * 10, Location = "Assembly Line A" }; _context.SensorReadings.Add(newReading); _context.SaveChanges(); // 立即写入数据库 // Local 集合自动添加,BindingSource 自动刷新 UI statusStrip1.Items[0].Text = $"已添加,共 {_context.SensorReadings.Local.Count} 条"; }说明:
SaveChanges()是同步阻塞调用,对 SQLite(本地文件)足够快,无需强制异步(SaveChangesAsync在 SQLite 上无实质性能提升,反而增加复杂度)。- 新增后
_context.SensorReadings.Local自动包含新实体,BindingSource检测到变化,DataGridView实时追加一行——这就是 ORM + BindingSource 的威力:UI 与数据模型零耦合,改模型即改界面。
4.3 支持双击编辑:利用 DataGridView 的 CurrentCellDirtyStateChanged 事件
WinFormDataGridView默认不支持单元格内双击编辑(需先选中再 F2)。要实现“双击即编辑”,监听CellDoubleClick并手动触发编辑:
private void dgReadings_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex >= 0 && e.ColumnIndex >= 0) { dgReadings.CurrentCell = dgReadings[e.ColumnIndex, e.RowIndex]; dgReadings.BeginEdit(true); } } // 捕获编辑结束,同步到实体 private void dgReadings_CellEndEdit(object sender, DataGridViewCellEventArgs e) { var row = dgReadings.Rows[e.RowIndex]; var reading = row.DataBoundItem as SensorReading; if (reading != null) { try { _context.SaveChanges(); // 提交变更 } catch (Exception ex) { MessageBox.Show($"保存失败:{ex.Message}"); // 回滚:重新加载该行数据 LoadData(); } } }注意:
CellEndEdit仅在用户修改后离开单元格时触发。若用户改完直接关窗,需在窗体关闭前调用SaveChanges()(见 5.3)。
5. 避坑指南:SQLite + EF Core 在 WinForm 中的 5 个血泪经验
5.1 现象:程序启动时报错 “The database file is locked”
原因:多个 DbContext 实例同时打开同一 SQLite 文件,且未正确释放连接。常见于:
- 在
MainForm构造函数中new SQLiteDbContext()(绕过 DI 容器); - 在
BackgroundWorker或Timer中另起 DbContext; SaveChanges()后未及时释放(虽 EF Core 会自动释放,但长事务会锁表)。
解决:- 严格使用 DI 容器注入
SQLiteDbContext,生命周期设为Scoped; - 避免在非 UI 线程中直接操作 DbContext(SQLite 是线程安全的,但 EF Core 的
DbContext不是); - 如需后台任务,用
Task.Run(() => { using var ctx = new SQLiteDbContext(...); ... })确保using释放。
5.2 现象:DataGridView 显示时间字段为 “1/1/0001 12:00:00 AM”
原因:SQLite 存储DateTime为TEXT,但 EF Core 未正确解析 ISO8601 字符串(如"2024-05-20T14:30:00"),反序列化失败回退为default(DateTime)。
解决:
- 在
OnModelCreating中显式指定HasColumnType("TEXT")(已做); - 确保
Timestamp属性有默认值= DateTime.Now(已做); - 若仍出错,检查数据库中该字段是否为空字符串
""(SQLite 允许NULL,但DateTime属性不可空),可在SensorReading中加[Required]并设默认值。
5.3 现象:关闭窗体后,未保存的编辑丢失
原因:BindingSource修改了Local集合,但未调用SaveChanges()。DataGridView的CellEndEdit仅在单元格离开时触发,用户可能直接点 X 关闭。
解决:
在MainForm_FormClosing事件中强制保存:
private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (_context.ChangeTracker.HasChanges()) { var result = MessageBox.Show("有未保存的更改,是否保存?", "确认", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); if (result == DialogResult.Yes) { try { _context.SaveChanges(); } catch (Exception ex) { MessageBox.Show($"保存失败:{ex.Message}"); e.Cancel = true; // 阻止关闭,让用户处理 } } else if (result == DialogResult.Cancel) { e.Cancel = true; } } }5.4 现象:DB Browser for SQLite 打开sensor_data.db显示乱码或中文字段为空
原因:SQLite 默认使用 UTF-8,但某些旧版 DB Browser(< 3.12)在 Windows 上默认用 ANSI 编码读取。
解决:
- 升级 DB Browser to SQLite 到最新版(官网下载);
- 或在 DB Browser 中:
File → Open Database → 选择文件 → Encoding: UTF-8; - 代码中确保字符串字段(如
SensorId,Location)在 C# 中为 Unicode,EF Core 自动处理 UTF-8 编码。
5.5 现象:发布为单文件(PublishSingleFile=true)后,SQLite 报错 “Unable to load DLL 'e_sqlite3'”
原因:SQLite 的原生库e_sqlite3.dll未被单文件打包包含。
解决:
在.csproj中添加:
<PropertyGroup> <PublishTrimmed>false</PublishTrimmed> <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> </PropertyGroup>并确保System.Data.SQLite.Core包已安装(它包含runtimes/win-x64/native/e_sqlite3.dll等)。发布时会将原生库解压到临时目录,EF Core 可定位。
6. 进阶技巧:加密数据库、批量导入与离线同步策略
6.1 用 SQLCipher 加密 SQLite 文件:保护敏感数据
SQLite 原生不支持加密,但 SQLCipher 是其最成熟的加密扩展。.NET 生态中,Microsoft.Data.Sqlite(非 EF Core 提供程序)原生支持 SQLCipher,但 EF Core 需换用Microsoft.Data.Sqlite.Core+ 自定义提供程序。更稳妥的做法是:用Microsoft.Data.Sqlite直接操作加密库,EF Core 仍用于业务模型。
步骤如下:
安装包:
dotnet add package Microsoft.Data.Sqlite.Core --version 8.0.8 dotnet add package SQLitePCLRaw.bundle_e_sqlcipher --version 2.1.10创建加密连接字符串(替换
OnConfiguring):protected override void OnConfiguring(DbContextOptionsBuilder options) { var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sensor_data_encrypted.db"); options.UseSqlite($"Data Source={dbPath};Password=mySecretKey123;"); }
注意:
Password=参数启用 SQLCipher,mySecretKey123是密钥。首次运行会自动创建加密数据库;已有明文库需用sqlcipher命令行工具转换。密钥切勿硬编码,应从配置文件或环境变量读取(如Configuration["Db:Password"])。
6.2 批量插入万级数据:绕过 EF Core 变更跟踪提升 10 倍速度
EF Core 的AddRange()对万级数据很慢(每条都走变更跟踪)。生产环境推荐用原生SqliteCommand批量插入:
public async Task BulkInsertReadings(List<SensorReading> readings) { var connectionString = $"Data Source={Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sensor_data.db")}"; using var connection = new SqliteConnection(connectionString); await connection.OpenAsync(); using var transaction = connection.BeginTransaction(); using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO SensorReadings (SensorId, Temperature, Humidity, Timestamp, Location) VALUES (@sensorId, @temp, @humid, @ts, @loc)"; var sensorParam = command.Parameters.Add("@sensorId", SqliteDbType.Text); var tempParam = command.Parameters.Add("@temp", SqliteDbType.Real); var humidParam = command.Parameters.Add("@humid", SqliteDbType.Real); var tsParam = command.Parameters.Add("@ts", SqliteDbType.Text); var locParam = command.Parameters.Add("@loc", SqliteDbType.Text); foreach (var r in readings) { sensorParam.Value = r.SensorId; tempParam.Value = r.Temperature; humidParam.Value = r.Humidity; tsParam.Value = r.Timestamp.ToString("o"); // ISO8601 locParam.Value = r.Location; await command.ExecuteNonQueryAsync(); } transaction.Commit(); }性能对比(10,000 条):
- EF Core
AddRange + SaveChanges:约 3200ms- 原生
SqliteCommand批量:约 320ms
关键:复用command和参数,避免重复解析 SQL。
6.3 离线同步设计:当需要多端数据合并时
WinForm 工具常需在多台设备间同步数据(如巡检员手机导出 CSV,回办公室导入主库)。纯 SQLite 不支持分布式事务,但可用“时间戳 + GUID”策略:
| 字段 | 类型 | 说明 |
|---|---|---|
Id | INTEGER PRIMARY KEY | 本地自增,仅用于外键关联 |
GlobalId | TEXT | Guid.NewGuid().ToString(),全局唯一,用于合并去重 |
CreatedAt | TEXT | 创建时间(UTC),用于冲突判断 |
UpdatedAt | TEXT | 最后更新时间(UTC) |
同步逻辑:
- 导出端:
SELECT * FROM SensorReadings WHERE UpdatedAt > @lastSyncTime; - 导入端:对每条记录,
INSERT OR IGNOREGlobalId;若存在,则UPDATE ... WHERE GlobalId = ? AND UpdatedAt > (SELECT UpdatedAt FROM ...); - 记录
@lastSyncTime为本次同步最大UpdatedAt。
此方案无需中心服务器,适合 USB 传输、邮件附件等弱网场景。
我写过三个工业现场的 WinForm 数据工具,全用这套组合。最深的教训是:别在 SQLite 上建外键级联(ON DELETE CASCADE),它在 EF Core 迁移中生成的 SQL 有 Bug,会导致DROP TABLE失败;宁可用代码层维护关系。还有就是,永远在AppDomain.CurrentDomain.BaseDirectory下操作数据库文件——我曾因用Environment.GetFolderPath指向Documents,导致客户双击桌面快捷方式时数据库写到奇怪位置,花了两小时才定位。
希望帮到你。
本文还有配套的精品资源,点击获取