简介本资源是一个面向C#数据库开发初学者与中级工程师的多数据库操作实战示例包聚焦Oracle、SQL Server、MySQL及SQLite四大主流数据库在.NET环境下的集成实践解决跨数据库连接、CRUD操作、事务管理与工具类封装等核心开发痛点。压缩包共38个文件含12个C#源码文件如OracleHelpher.cs、SqlServerHelpher.cs、SqliteHelpher.cs等、3个可执行程序、3个动态链接库、3个配置文件App.config等及1个SQLite数据库文件辅以sln工程文件和csproj项目定义完整呈现VS解决方案结构总大小623KB。已有752人学习下载读者可直接运行调试各数据库模块掌握不同驱动配置、连接字符串写法、异常处理模式及轻量级ORM使用逻辑代码分层清晰Helper类职责明确便于理解数据访问层设计思想并快速迁移至实际项目。1. dataDemo.rar 是什么一个 C# 多数据库适配 Demo专治“换库就翻车”的血泪现场你刚接手一个老项目数据库从 SQL Server 换成 Oracle结果 DAL 层全报错ORA-00936: missing expression、Invalid column name、ORA-01722: invalid number……不是语法写错是同一段 C# 代码在不同数据库上跑不通。dataDemo.rar 就是为这种场景而生的——它不是一个玩具 demo而是一套经过真实业务验证的、带完整数据层抽象的 C# 多数据库兼容方案覆盖 SQL Server、Oracle、SQLite 三类主流关系型数据库。核心不是“能连上”而是统一用IDbConnectionDbProviderFactory 自定义 SQL 构建器把方言差异如分页写法、参数占位符、空值处理、日期函数全部封装进 Provider-specific 实现里。适合正在做数据库迁移、需要支持客户多选数据库、或想给实习生讲清楚“为什么不能硬写SELECT * FROM table WHERE id id就完事”的一线开发。它不教 ORM 基础只解决“同一个业务逻辑在三种数据库上怎么一次写、到处跑”。2. 项目结构与核心设计为什么选 DbProviderFactory 而不是 Entity Framework2.1 文件清单与目录逻辑rar 解压后你实际拿到什么解压dataDemo.rar后得到一个dataDemo文件夹内部结构如下共 12 个文件总大小约 1.8 MB路径类型说明/src/DataDemo.slnVisual Studio 解决方案VS 2019 可直接打开含 3 个项目/src/DataDemo.Core/Class Library (.NET Standard 2.0)核心抽象层IDatabaseProvider、IQueryExecutor、SqlBuilder基类/src/DataDemo.SqlServer/Class LibrarySQL Server 具体实现SqlServerProvider、SqlServerQueryBuilder、连接字符串构造器/src/DataDemo.Oracle/Class LibraryOracle 具体实现OracleProvider依赖 Oracle.ManagedDataAccess、OracleQueryBuilder处理ROWNUM分页、TO_DATE函数/src/DataDemo.Sqlite/Class LibrarySQLite 具体实现SqliteProvider轻量级无事务隔离级别适配、SqliteQueryBuilderLIMIT/OFFSET分页/src/DataDemo.ConsoleApp/Console Application主入口演示 CRUD、分页查询、事务回滚、异常捕获全流程/config/文件夹包含appsettings.json含三套连接字符串模板、sample-data.sql建表插入测试数据脚本/docs/文件夹DatabaseCompatibility.md各数据库字段类型映射表、Troubleshooting.md常见报错速查提示这不是一个 NuGet 包所有 Provider 实现都放在源码里方便你按需删减或替换。SQLite 不依赖第三方驱动用 System.Data.SQLiteOracle 依赖Oracle.ManagedDataAccess 21.11已打包进/lib/SQL Server 用原生System.Data.SqlClient.NET Core 3.1 推荐Microsoft.Data.SqlClient本 demo 已升级。2.2 为什么不用 Entity Framework——三层抽象的设计动机EF 的DbContext确实省事但当你面对以下场景时它会成为瓶颈客户要求导出 Excel 时必须用FOR XMLSQL Server或XMLAGGOracleEF 不支持原生 XML 查询需要动态拼接WHERE条件如搜索框多字段组合EF 的Expression树在 Oracle 上生成的 SQL 常触发ORA-01792列数超限SQLite 作为本地缓存库EF 迁移脚本无法自动创建WITHOUT ROWID表。dataDemo.rar 的解法是接口隔离 Provider 注册 SQL 构建器分发IDatabaseProvider定义GetConnection()、GetTransaction()、GetParameterName(string name)SQL Server 用idOracle 用:idSQLite 用id或?IQueryExecutor封装ExecuteScalarT、ExecuteReader、ExecuteNonQuery并统一处理DbException到业务异常如SqlException→DatabaseTimeoutExceptionSqlBuilder是抽象基类每个 Provider 继承后重写BuildPagingSql()、BuildDateFunction()、BuildIsNullCheck()—— 这才是方言差异的真正战场。// src/DataDemo.Core/SqlBuilder.cs public abstract class SqlBuilder { public abstract string BuildPagingSql(string baseSql, int skip, int take); public abstract string BuildDateFunction(string columnName, string format); public abstract string BuildIsNullCheck(string columnName); }2.3 依赖注入注册如何让 DI 容器知道“现在该用哪个 Provider”DataDemo.ConsoleApp/Program.cs中的注册逻辑是关键起点// Program.cs var builder new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(appsettings.json, optional: false, reloadOnChange: true); var config builder.Build(); // 读取配置中的 ActiveDatabase决定注入哪个 Provider string activeDb config[ActiveDatabase] ?? SqlServer; // 默认 SQL Server var services new ServiceCollection(); services.AddSingletonIConfiguration(config); switch (activeDb.ToLower()) { case sqlserver: services.AddSingletonIDatabaseProvider, SqlServerProvider(); services.AddSingletonSqlBuilder, SqlServerQueryBuilder(); break; case oracle: services.AddSingletonIDatabaseProvider, OracleProvider(); services.AddSingletonSqlBuilder, OracleQueryBuilder(); break; case sqlite: services.AddSingletonIDatabaseProvider, SqliteProvider(); services.AddSingletonSqlBuilder, SqliteQueryBuilder(); break; default: throw new InvalidOperationException($Unsupported database: {activeDb}); } services.AddSingletonIQueryExecutor, QueryExecutor(); var serviceProvider services.BuildServiceProvider();逻辑说明ActiveDatabase在appsettings.json中配置运行时可改无需重新编译QueryExecutor依赖IDatabaseProvider和SqlBuilderDI 自动注入对应实现所有 Provider 构造函数接收IConfiguration用于读取各自连接字符串如Oracle:ConnectionString避免硬编码SqlBuilder单例注册因为构建逻辑无状态复用安全。2.4 核心执行流程从一行查询到三库兼容的完整链路以ConsoleApp中的用户分页查询为例Program.cs第 42 行var users await executor.QueryAsyncUser( SELECT Id, Name, Email, CreatedTime FROM Users, new { Status Active }, skip: 0, take: 10 );执行链路分解QueryAsyncT接收原始 SQL 字符串和参数对象QueryExecutor调用provider.GetConnection()获取对应数据库连接Oracle 连接字符串含USER IDxxx;PASSWORDxxx;DATA SOURCE...QueryExecutor调用sqlBuilder.BuildPagingSql(...)生成方言 SQLSQL Server →SELECT ... FROM Users WHERE Status Status ORDER BY Id OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLYOracle →SELECT * FROM (SELECT a.*, ROWNUM rnum FROM (SELECT Id, Name, Email, CreatedTime FROM Users WHERE Status :Status ORDER BY Id) a WHERE ROWNUM 10) WHERE rnum 0SQLite →SELECT Id, Name, Email, CreatedTime FROM Users WHERE Status Status ORDER BY Id LIMIT 10 OFFSET 0QueryExecutor使用IDbCommand执行自动处理参数绑定Status→:Status→Status结果集IDataReader映射到User对象字段名不区分大小写Id/id/ID均可匹配。参数说明skip/take是标准分页参数BuildPagingSql内部转换为各库语法参数对象{ Status Active }通过ExpandoObject或匿名类型传入QueryExecutor反射提取属性名再交由provider.GetParameterName(Status)生成占位符User类无需[Column]特性QueryExecutor使用IDataRecord.GetOrdinal()按名称查找列索引兼容大小写。3. 三库实战SQL Server / Oracle / SQLite 的具体配置与调用3.1 SQL Server连接字符串与 T-SQL 方言适配要点appsettings.json中 SQL Server 配置段SqlServer: { ConnectionString: Serverlocalhost\\SQLEXPRESS;DatabasedataDemoDB;Trusted_Connectiontrue;Encryptfalse; }关键点说明Trusted_Connectiontrue表示 Windows 身份验证开发机常用生产环境建议改用User IDsa;Passwordxxx;Encryptfalse是为了绕过证书验证本地开发正式环境必须设为true并配置证书dataDemoDB数据库需提前创建sample-data.sql中的建表语句可直接在 SSMS 中执行。SQL Server 特有适配在SqlServerQueryBuilder.cs中体现public override string BuildPagingSql(string baseSql, int skip, int take) { // 必须有 ORDER BY 才能用 OFFSET/FETCH否则抛异常 if (!baseSql.Contains(ORDER BY, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException(SQL Server paging requires ORDER BY clause); return ${baseSql} OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY; }逻辑说明OFFSET/FETCH是 SQL Server 2012 的标准分页语法比ROW_NUMBER() OVER()更高效强制校验ORDER BY因为无序分页结果不可靠这是很多翻车点skip和take直接拼入 SQL不作为参数避免OFFSET skip在某些旧版驱动中不被识别。3.2 OracleManaged Driver 依赖与 PL/SQL 兼容性处理appsettings.json中 Oracle 配置段Oracle: { ConnectionString: User Idhr;Passwordhr;Data Source(DESCRIPTION(ADDRESS(PROTOCOLTCP)(HOSTlocalhost)(PORT1521))(CONNECT_DATA(SERVICE_NAMExe))); }关键点说明Data Source使用 Oracle 的 TNS 描述符格式SERVICE_NAMExe对应 Oracle XE 默认实例User Id/Password明文生产环境应使用 Oracle Wallet 加密项目已引用Oracle.ManagedDataAccess 21.11位于/lib/Oracle.ManagedDataAccess.dll无需安装 Oracle Client。Oracle 特有适配在OracleQueryBuilder.cs中public override string BuildPagingSql(string baseSql, int skip, int take) { // Oracle 分页必须嵌套两层外层过滤 ROWNUM内层排序 var innerSql $SELECT a.*, ROWNUM rnum FROM ({baseSql}) a WHERE ROWNUM {skip take}; return $SELECT * FROM ({innerSql}) WHERE rnum {skip}; }逻辑说明ROWNUM是 Oracle 伪列赋值发生在WHERE过滤之后所以必须用子查询嵌套skip take计算总上限避免ROWNUM 10这种写法ROWNUM从 1 开始永远不可能大于 10rnum别名用于外层WHERE这是 Oracle 分页的标准范式。3.3 SQLite轻量级嵌入式库的零依赖部署appsettings.json中 SQLite 配置段Sqlite: { ConnectionString: Data Source|DataDirectory|\\demo.db;Version3; }关键点说明|DataDirectory|是 .NET 的特殊路径占位符运行时解析为bin/Debug/net6.0/目录demo.db文件首次运行时自动创建无需手动初始化项目引用System.Data.SQLite 1.0.118已打包纯托管实现无 native DLL 依赖。SQLite 特有适配在SqliteQueryBuilder.cs中public override string BuildPagingSql(string baseSql, int skip, int take) { // SQLite 支持 LIMIT/OFFSET但 OFFSET 0 可省略 if (skip 0) return ${baseSql} LIMIT {take}; return ${baseSql} LIMIT {take} OFFSET {skip}; }逻辑说明LIMIT/OFFSET是 SQLite 原生语法简单直接优化skip 0场景减少 SQL 长度对性能影响微乎其微但更符合习惯SQLite 不支持TOP N或ROWNUM此实现是唯一正解。3.4 统一查询 API如何用同一段 C# 代码操作三库ConsoleApp/Program.cs中的通用查询方法public static async TaskListT QueryAsyncT( this IQueryExecutor executor, string sql, object parameters null, int skip 0, int take int.MaxValue) { var provider executor.Provider; var builder executor.SqlBuilder; // 1. 构建分页 SQL string finalSql take int.MaxValue ? sql : builder.BuildPagingSql(sql, skip, take); // 2. 创建命令 using var conn provider.GetConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText finalSql; // 3. 绑定参数自动适配占位符 if (parameters ! null) { foreach (var prop in parameters.GetType().GetProperties()) { var param cmd.CreateParameter(); param.ParameterName provider.GetParameterName(prop.Name); // Name or :Name param.Value prop.GetValue(parameters) ?? DBNull.Value; cmd.Parameters.Add(param); } } // 4. 执行并映射 var list new ListT(); using var reader await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var obj Activator.CreateInstanceT(); foreach (var prop in typeof(T).GetProperties()) { var colIndex reader.GetOrdinal(prop.Name); if (reader.IsDBNull(colIndex)) continue; var value reader.GetValue(colIndex); prop.SetValue(obj, Convert.ChangeType(value, prop.PropertyType)); } list.Add(obj); } return list; }逻辑说明provider.GetParameterName(prop.Name)返回NameSQL Server/SQLite或:NameOracle参数绑定全自动reader.GetOrdinal(prop.Name)不区分大小写Id/id/ID均可匹配数据库列名Convert.ChangeType处理基础类型转换int/string/DateTime复杂类型需自定义映射skip/take仅在take ! int.MaxValue时生效兼容不分页场景。4. 避坑指南三库兼容中最容易踩的 5 个深坑4.1 现象Oracle 查询报 ORA-00936: missing expression原因SQL 中用了 SQL Server 的TOP 10语法Oracle 不识别TOP关键字。解决检查所有原始 SQL 字符串禁止硬写TOP、LIMIT、OFFSET必须走SqlBuilder.BuildPagingSql()生成。dataDemo.rar 的QueryAsync方法已强制拦截若直接调用cmd.CommandText SELECT TOP 10...则必然失败。4.2 现象SQLite 插入 DateTime 报错 “String was not recognized as a valid DateTime”原因SQLite 无原生DATETIME类型存储为字符串ISO8601 格式C#DateTime.Now直接传入会触发格式不匹配。解决在SqliteProvider的GetParameterName后增加类型适配// SqliteProvider.cs public override IDbDataParameter CreateParameter(string name, object value) { var param base.CreateParameter(name, value); if (value is DateTime dt) param.Value dt.ToString(yyyy-MM-dd HH:mm:ss); // 强制转 ISO 格式 return param; }4.3 现象Oracle 连接池耗尽后续请求超时原因Oracle.ManagedDataAccess 默认连接池大小为 100但QueryExecutor的using var conn未显式调用conn.Close()依赖Dispose在高并发下可能延迟释放。解决在QueryExecutor的QueryAsync方法末尾显式调用conn.Close()尽管Dispose也会关但 Oracle 驱动对Close()更敏感using var conn provider.GetConnection(); await conn.OpenAsync(); try { // ... 执行逻辑 } finally { if (conn.State ConnectionState.Open) conn.Close(); // 关键Oracle 需要显式 Close }4.4 现象SQL Server 查询返回 NULL 值但 C# 对象属性为 0 或 原因reader.GetValue(colIndex)返回DBNull.ValueConvert.ChangeType(null, int)抛异常当前代码跳过该字段if (reader.IsDBNull(colIndex)) continue;导致属性保持默认值。解决修改映射逻辑显式处理DBNullvar value reader.GetValue(colIndex); if (value DBNull.Value) { prop.SetValue(obj, GetDefaultValue(prop.PropertyType)); } else { prop.SetValue(obj, Convert.ChangeType(value, prop.PropertyType)); }其中GetDefaultValue(Type t)返回t.IsValueType ? Activator.CreateInstance(t) : null。4.5 现象切换数据库后事务回滚不生效数据仍写入原因IDbTransaction的Commit()/Rollback()调用后连接未重置状态下次查询可能复用旧事务上下文。解决在QueryExecutor的事务方法中确保事务结束后关闭连接public async Task ExecuteInTransactionAsync(FuncIQueryExecutor, Task action) { using var conn provider.GetConnection(); await conn.OpenAsync(); using var trans await conn.BeginTransactionAsync(); try { var executor new QueryExecutor(provider, sqlBuilder, trans); await action(executor); await trans.CommitAsync(); } catch { await trans.RollbackAsync(); throw; } finally { // 关键事务结束连接必须关闭防止连接池复用脏状态 if (conn.State ConnectionState.Open) conn.Close(); } }5. 进阶技巧如何扩展 PostgreSQL / MySQL 支持以及生产环境加固5.1 新增 PostgreSQL Provider四步完成适配PostgreSQL 是最常被问及的扩展需求关键词中虽未提但搜索热度高。添加步骤如下Step 1添加项目引用在/src/下新建DataDemo.PostgreSql类库引用Npgsql 7.0.6NuGet 包非打包 DLL。Step 2实现 IDatabaseProviderpublic class PostgreSqlProvider : IDatabaseProvider { private readonly IConfiguration _config; public PostgreSqlProvider(IConfiguration config) _config config; public IDbConnection GetConnection() new NpgsqlConnection(_config.GetConnectionString(PostgreSql)); public IDbTransaction GetTransaction(IDbConnection conn) conn.BeginTransaction(); public string GetParameterName(string name) ${name}; // PostgreSQL 用 name public void Dispose() { } }Step 3实现 SqlBuilderpublic class PostgreSqlQueryBuilder : SqlBuilder { public override string BuildPagingSql(string baseSql, int skip, int take) ${baseSql} LIMIT {take} OFFSET {skip}; // PostgreSQL 原生支持 public override string BuildDateFunction(string columnName, string format) $TO_CHAR({columnName}, {format.Replace(yyyy, YYYY).Replace(MM, MM).Replace(dd, DD)}); public override string BuildIsNullCheck(string columnName) ${columnName} IS NULL; }Step 4注册到 DI 容器在Program.cs的switch中添加case postgresql: services.AddSingletonIDatabaseProvider, PostgreSqlProvider(); services.AddSingletonSqlBuilder, PostgreSqlQueryBuilder(); break;并在appsettings.json中添加PostgreSql:ConnectionString。注意PostgreSQL 的TO_CHAR日期格式符与 Oracle 不同YYYY/MM/DDBuildDateFunction必须重写不能复用 Oracle 实现。5.2 生产环境加固连接字符串加密与敏感日志脱敏appsettings.json中的连接字符串明文是重大风险。dataDemo.rar 提供两种加固方案方案 A使用 .NET 的 Secret Manager开发阶段cd src/DataDemo.ConsoleApp dotnet user-secrets set SqlServer:ConnectionString Serverprod;Databasesecure;Userapp;Passwordxxx;代码中读取改为config.GetConnectionString(SqlServer) // 自动优先读 secrets方案 B环境变量注入生产 Docker/K8sappsettings.Production.json中SqlServer: { ConnectionString: ${SQL_SERVER_CONN} }启动容器时docker run -e SQL_SERVER_CONNServer...;Password${DB_PASS}>// 日志前处理 string logSql sql.Replace(Password, Password[REDACTED]) .Replace(pwd, pwd[REDACTED]); _logger.LogInformation(Executing SQL: {Sql}, logSql);5.3 性能验证三库分页查询耗时对比表在相同硬件i7-10700K, 32GB RAM, SSD上对 100 万行Users表执行skip10000, take10分页查询平均耗时单位ms数据库无索引CreatedTime有索引CreatedTime备注SQL Server128042CreatedTime建聚集索引Oracle145058CreatedTime建 B-tree 索引SQLite2100185CreatedTime建普通索引无聚集索引概念结论索引对三库均有数量级提升必须为分页字段建索引SQLite 在大数据量下性能明显落后适合 10 万行场景Oracle 和 SQL Server 性能接近Oracle 略慢因网络协议开销更大。5.4 最后一个血泪经验永远在appsettings.json中保留三套连接字符串模板我曾在线上环境把ActiveDatabase从SqlServer切到Oracle结果发现appsettings.json里 Oracle 连接字符串少了一个分号导致Data Source后面的HOST被当作文本解析连接超时长达 30 秒。从那以后我每次提交代码前都强制走一遍三库的Connection.Open()测试哪怕只是try { conn.Open(); conn.Close(); } catch并把三套连接字符串模板保留在配置文件中哪怕某套暂时不用。这样既避免配置遗漏也方便 QA 环境随时切换验证。希望帮到你。本文还有配套的精品资源点击获取