简介:对于.NET Framework 4.5环境下需要接入PostgreSQL的开发者,这份726KB的压缩包提供了完整的连接API组件。资源共19个文件,核心是Npgsql.dll及其依赖的Mono.Security.dll,前者提供ADO.NET兼容的数据访问接口,后者用于SSL连接和证书验证等安全支持;同时包含Entity Framework适配器(Npgsql.EntityFramework.dll及Legacy版本)、3个XML文档和PDB调试符号,便于API查阅与错误定位。包内还附有README说明和LICENSE许可协议,可帮助了解安装配置与合规使用。已有892人学习,适合正在使用.NET 4.5构建PostgreSQL数据交互场景的中高级开发者,尤其需要Entity Framework集成或安全连接验证的工程。
1. 老 .NET Framework 4.5 项目连 PostgreSQL:先分清 Npgsql.dll 和 Mono.Security.dll 各管什么
在 .NET Framework 4.5 的老业务系统里接 PostgreSQL,绕不开标题里这两个 DLL:Npgsql.dll 是社区维护的 ADO.NET 数据提供程序,把 System.Data 接口翻译成 PostgreSQL 的 wire protocol;Mono.Security.dll 则是 Npgsql 2.x 时代自带的 SSL 支撑库,新版已经不需要。很多人把两个 DLL 一起拷进 bin,编译通过,部署到服务器却在 Open() 时抛 FileNotFoundException 或 SSL 握手失败,根子都在版本混搭。
下面按落地顺序讲:先选对 Npgsql 版本,再写出最小连接,然后逐个排查 SSL、SCRAM、DLL 加载三类高频坑,最后给一个验证连接与版本号的检查习惯。适合正在维护遗留系统的开发,也适合在 Windows 上做 PostgreSQL 适配的运维。
2. 版本选型:.NET Framework 4.5 能吃下哪一版 Npgsql
2.1 Npgsql 2.x / 3.x / 4.x:三条版本线的兼容边界
Npgsql 在 NuGet 上从 2.x 一路走到 7.x,但目标框架划分得很清楚。2.x 时代是给 .NET Framework 2.0/3.5/4.0 用的,最后一个版本停在 2.0.11 附近,最明显的特征是压缩包里同时给你 Npgsql.dll 和 Mono.Security.dll,SSL 功能由后者提供。3.x 是专门为 .NET Framework 4.5 重写的一代,从 3.0 开始去掉 Mono.Security 依赖,改用 System.Net.Security 走 TLS,这一代最后的小版本是 3.2.7。4.x 是又一次大重写,目标框架直接抬到 .NET Framework 4.5.2 和 .NET Standard 2.0,5.x/6.x 基本只服务 .NET Core / .NET 5+,到了 7.x 就不再碰 .NET Framework。
这里最容易踩的坑是:网上教程很多让直接 Install-Package Npgsql,结果把最新版装进了 .NET Framework 4.5 项目。编译可能过(包里的 netstandard 资产会被兼容过来),一运行就报程序集加载失败或要求更高运行时。判断标准很简单:csproj 里 TargetFrameworkVersion 是 v4.5,就用 3.2.7;如果项目能接受升到 v4.5.2 或更高,才轮得到 4.x。
| Npgsql 版本线 | 目标框架 | Mono.Security.dll | 与 PostgreSQL 兼容情况 |
|---|---|---|---|
| 2.0.x ~ 2.2.x | .NET 2.0 / 3.5 / 4.0 | 需要 | PG 9.x ~ 10.x 可跑,连 PG 14+ 默认 SCRAM 会认证失败 |
| 3.0.x ~ 3.2.x | .NET Framework 4.5 | 不需要 | PG 9.x ~ 15.x 均可,3.2.7 对 SCRAM 支持成熟 |
| 4.0.x | .NET Framework 4.5.2+ / .NET Standard 2.0 | 不需要 | 全面重写,参数、类型映射行为差异大 |
| 5.x ~ 7.x | .NET Core / .NET 5+ | 不需要 | 不再支持 .NET Framework |
提示:项目是 v4.5 还是 v4.5.2,以 csproj 里
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>这一行为准,别在 VS 属性页里凭印象判断。
很多遗留系统动不了框架版本,一抬就牵出第三方控件兼容问题,所以 .NET Framework 4.5 这条线的终点就是 3.2.7。如果你的团队能接受把目标框架抬到 4.5.2,用 4.x 在异步支持和类型映射上会更省心,但那属于"改框架"级别的工作量,不是加一个 DLL 能解决的。
2.2 Mono.Security.dll 从哪来,为什么新分发里看不到它
Mono.Security.dll 的来历要回到 Npgsql 2.x 的年代。那时 .NET Framework 2.0/3.5 在 SSL 客户端证书、TLS 握手这些场景上支持不完整,尤其是跨平台跑在 Mono 托管环境里的时候,Npgsql 直接复用了 Mono 项目编译出来的 Security 程序集来完成 SSL 传输层。所以它既不是 PostgreSQL 官方文件,也不是 Npgsql 3.x 之后的组成部分。
判断手里资源属于哪个年代有个土办法:凡是让"同时引用 Npgsql.dll 和 Mono.Security.dll"的教程或安装包,基本是 2013 年前后的 Npgsql 2.x 分发;到了 3.x,包里只有一个 Npgsql.dll(外加 Npgsql.xml 注释文档)。如果你在一个 3.x 项目里硬把 Mono.Security.dll 留着,两个程序集之间并没有依赖关系,多数情况是白占空间;反过来,2.x 项目少了 Mono.Security.dll,开 SSL 连接时必炸。这个 DLL 该不该留,完全取决于 Npgsql 的大版本,不能靠"多个文件多份保险"的思维来定。
2.3 用 NuGet 锁版本,还是手工拷贝 DLL:两条落地点
第一条路,也是我最推荐的路,是 NuGet 锁版本安装:
Install-Package Npgsql -Version 3.2.7在 VS2013/2015/2017 的 Package Manager Console 里执行,强制装到 3.2.7,不让 NuGet 自动解析成 4.x 或更高。装完检查 packages.config 或 csproj 里的引用版本号是不是 3.2.7.0,同时确认没有把 2.x 的老引用残留下来。升级老项目时如果 bin 目录里躺着旧的 Npgsql.dll 和 Mono.Security.dll,先清空 bin 再重新生成,避免新旧程序集混在输出目录里。
第二条路是离线环境手工拷贝。从 nuget.org 下载 Npgsql 3.2.7 的 .nupkg,把扩展名改成 .zip 解压,里面会有 lib/net45/Npgsql.dll 和 lib/net45/zh-Hans 等资源目录。取 net45 目录下的 DLL 拷到项目 bin 即可。注意别拿 lib/netstandard2.0 目录下的文件放进 .NET Framework 4.5 项目,某些场景能编译,但运行时行为不可控。
装完如果还报程序集加载失败,检查 app.config 或 web.config 里的 assemblyBinding 节点。有些老项目升级过一次 NuGet,配置文件里残留着旧版本的 bindingRedirect,重定向目标是 3.0.0.0,实际 DLL 是 3.2.7.0,运行时就找不到程序集。把 redirect 删除或改成 3.2.7.0 再试。另外确认 packages 目录跟着仓库走,别让 csproj 的 HintPath 指向本机私有缓存路径。
还有一句部署提醒:如果是内网离线 Windows 环境,装 Npgsql 之前先把 PostgreSQL 服务端装好、数据目录初始化完成。Windows 上"postgresql数据库启动服务失败在等待服务器启动时超时"这类报错通常是服务端没起来,和客户端 DLL 没有关系,别在客户端排查上耗时间。
3. 最小连接落地:连接字符串、池参数与第一个查询
3.1 连接字符串:必填项与推荐默认值
Npgsql 的连接字符串和 SqlClient 不是一个方言,键名不区分大小写,但建议统一写法。必填项是 Server、Port、Database、User Id、Password;别把 Password 明文写在 app.config 里,至少在部署前用配置加密或环境变量顶替。下面是我在 .NET Framework 4.5 项目里常用的底线配置:
Server=127.0.0.1;Port=5432;Database=appdb;User Id=app_user;Password=your_password;Pooling=true;Min Pool Size=1;Max Pool Size=20;Timeout=15;Command Timeout=30;SSL Mode=Prefer;逐个说明:Server 写 IP 而不是 localhost,避免部分环境下 DNS 解析拖慢建连;Port 默认 5432,换了端口必须显式写;Pooling 在 Npgsql 3.x 默认就是 true,写上是为了让接手的人一眼看到;Timeout 是建连超时(秒),15 够用;Command Timeout 是单条 SQL 的执行超时,默认偏短,我一般调到 30,真正慢的 SQL 单独在命令对象上加大。SSL Mode 在 3.x 里取值 Disable / Prefer / Require,内网且链路可信时用 Prefer,公网必须 Require。
字符集方面,连接串不用刻意设 Encoding,Npgsql 默认走 UTF-8。中文乱码多数是 PostgreSQL 数据库本身的编码不是 UTF8,属于建库问题,不是连接问题。
提示:连接字符串的键名在 Npgsql 3.x 中按 PascalCase 或空格分隔两种写法都认,例如 CommandTimeout 与 Command Timeout 等价,但别在同一串里混合风格,排错时纯属浪费精力。
3.2 打开连接并执行第一个查询:最小 C# 代码
直接贴一个能编译的最小控制台程序。关键在于用 NpgsqlConnectionStringBuilder 构造连接串,而不是手拼字符串;手拼最容易在密码含特殊字符时翻车。
using System; using Npgsql; class Program { static void Main(string[] args) { var builder = new NpgsqlConnectionStringBuilder { Host = "127.0.0.1", // 等价于 Server,两种写法都行 Port = 5432, Database = "appdb", UserName = "app_user", Password = "your_password", Pooling = true, MaxPoolSize = 20, Timeout = 15, CommandTimeout = 30 }; using (var conn = new NpgsqlConnection(builder.ConnectionString)) { conn.Open(); using (var cmd = new NpgsqlCommand("SELECT version();", conn)) { Console.WriteLine(cmd.ExecuteScalar()); } } } }代码逻辑分三段:Builder 把参数结构化成标准连接串;using 保证连接 Close 后归还连接池;命令对象同样用 using 释放。这里有一个 .NET Framework 4.5 上常见的黑匣子行为:Open() 抛异常时,内层异常往往比外层更有用,比如 SSL 证书问题会包在 AuthenticationException 里,排查先看 InnerException,别盯着最外层一句错误码猜。
带参数的查询要避免字符串拼接。Npgsql 3.x 支持 @ 和 : 两种占位符,参数用 AddWithValue 或显式 NpgsqlParameter 都可以:
using (var conn = new NpgsqlConnection(builder.ConnectionString)) { conn.Open(); using (var cmd = new NpgsqlCommand( "SELECT id, name FROM users WHERE created_at > @since ORDER BY id LIMIT @limit;", conn)) { cmd.Parameters.AddWithValue("since", DateTime.UtcNow.AddDays(-7)); cmd.Parameters.AddWithValue("limit", 100); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { Console.WriteLine($"{reader.GetInt32(0)}\t{reader.GetString(1)}"); } } } }AddWithValue 的隐患是类型推断:DateTime 参数会被映射成 timestamp 或 timestamptz,取决于列类型;如果列是 timestamptz,传进来的 DateTime.Kind 是 Utc 还是 Local 会直接影响结果和时区偏移。所以宁可显式 NpgsqlDbType,也别在类型推断上赌。
3.3 连接池、超时与 Keepalive:三个最常调的参数
连接池是 Npgsql 默认开启的,Max Pool Size 默认值不高,业务并发一大就爱报 Timeout expired。Min Pool Size 设 1 的作用是让进程启动后先建一条连接做预热,避免第一个请求承担建连开销。Keepalive 参数(单位秒)在 3.x 里存在,默认 0 表示不启用,我一般设 15 到 30,专门对付防火墙或 NAT 把空闲连接静默回收的问题:连接看起来还在,对端实际已断开,下次请求直接抛异常。服务端对应的是 postgresql.conf 里 tcp_keepalives_idle,两边配合才能彻底解决。
另一个常被当玄学的坑是 Command Timeout。很多人把连接池耗尽归因到并发,实际是一条慢 SQL 把连接占满了。看 pg_stat_activity 里的 state 列:如果是 idle in transaction,说明事务没提交还占着连接;如果是 active 且超过 30 秒,优先排查 SQL 和索引,而不是网络。连接泄漏的话,代码里强制用 using 或 try/finally Dispose,泄漏一个少一个池内连接,这个问题在长跑服务里比 SSL 更致命。
4. 高频踩坑与排查:SSL 握手、SCRAM 认证与 DLL 加载失败
4.1 现象一:Open() 就抛 SSL 或 TLS 异常
现象是本地连开发库正常,换到配了 SSL 的生产库,Open() 直接抛 AuthenticationException,内层可能是 "The remote certificate is invalid according to the validation procedure",也可能是 TLS 握手失败。原因分两类,必须分开判断:一类是 PostgreSQL 服务器证书是自签名或域名不匹配,Npgsql 3.x 默认校验服务端证书链;另一类是 .NET Framework 4.5 的 SslStream 默认协商 TLS 1.0,而新版 PostgreSQL 在 OpenSSL 1.1.1+ 的发行版上往往不再接受 TLS 1.0/1.1。
解决第一步是定位类别:把连接串临时改成 SSL Mode=Disable,如果问题消失,说明是证书或 TLS 协商问题,而不是密码或网络问题。内网自签证书场景下,Npgsql 3.x 提供事件回调:
conn.ValidateRemoteCertificateCallback += (sender, cert, chain, errors) => true;这个回调返回 true 表示无条件信任,只能在明确知道证书来源的内网环境用,公网这样做等于把传输层安全关掉。TLS 版本问题的解法有两个层次:代码里在建连前执行 System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;或者直接在 Windows 注册表启用 Schannel 的 TLS 1.2 客户端并重启。代码方式多数情况生效,注册表方式要重启且影响整机其他程序,改动前先备份。
4.2 现象二:报不支持的认证方式或密码错误
连接 PG 14 以上的库时报 "authentication method 10 not supported",或者密码明明对却说认证失败。原因是 PostgreSQL 14 起默认的密码认证方式是 scram-sha-256,而 Npgsql 2.x 和 3.x 早期小版本只实现 md5。升级到 3.2.7 是最省事的解法,这一版对 SCRAM 的处理已经稳定。如果项目连 3.2.7 都升不动,只能从服务端迁就:把 pg_hba.conf 里对应 host 行的 auth-method 从 scram-sha-256 改成 md5,保存后执行 pg_reload_conf()。
这里有一个容易漏的细节:如果该用户当初是用 scram 方式保存的密码,光改 pg_hba.conf 没用,因为服务端存的是 SCRAM 散列,不是 MD5 散列。需要把 postgresql.conf 里的 password_encryption 临时设为 md5,重新执行 ALTER USER xxx PASSWORD '新密码';,再改回 scram。整个过程记一条变更记录,避免几天后别人排查时一头雾水。
4.3 现象三:部署机报 FileNotFoundException,缺 Mono.Security.dll
本地开发机编译运行正常,发布到服务器后在第一次 Open() 时报 FileNotFoundException,内容是找不到 Mono.Security.dll。这种报错在 .NET Framework 项目里太典型:开发机 bin 里有,发布时被某个排除规则过滤掉了,或者教程只让引用 Npgsql.dll 忘了它的依赖。判断依赖关系最直接的办法是用程序集绑定日志工具 fuslogvw.exe,把加载失败的 DLL 名称和路径打出来,而不是瞎猜。
解决要看版本:如果是 Npgsql 2.x,把 Npgsql.dll 和 Mono.Security.dll 放在同一目录并保持同一压缩包里的版本,两个都要引用;如果是 3.x,应当删除对 Mono.Security.dll 的引用,重新从 NuGet 拉干净的 3.2.7,然后清空 bin 再发布。最怕的是 bin 里同时躺着 2.x 和 3.x 的 Npgsql.dll 以及一个来历不明的 Mono.Security.dll,程序集加载跟抽盲盒一样,删干净重来是唯一出路。
4.4 现象四:连接偶发超时,池被耗尽
现象是服务跑一阵子后请求陆续报 Timeout expired,重启服务又正常,过段时间复发。先看 pg_stat_activity:如果大量连接处于 idle in transaction,基本可以断定是事务没提交就返回了结果,连接被长期占用;如果连接数顶到池上限且都是 active,则是慢 SQL 堆积。前者是代码问题,后者是 SQL 或索引问题,别一上来就调 Max Pool Size 掩盖。
应急手段可以临时调大 Max Pool Size,但这只是后悔药,治标不治本。正确姿势是检查所有连接是否在 using 或 try/finally 里释放,事务分支里有没有遗漏 Commit / Rollback,以及是否把网络请求写进了事务内。连接问题特别严重时,可以调用 NpgsqlConnection.ClearAllPools() 立刻回收空闲连接,但正在使用的连接不受影响,别指望它解决泄漏。
4.5 现象五:服务端没启动,误报连接超时
最后一条最容易迷惑人。客户端报连接超时或 Timeout expired,直觉是网络或连接池问题,结果 Windows 事件日志里写着"postgresql数据库启动服务失败在等待服务器启动时超时"。这是 PostgreSQL 服务进程在启动阶段挂掉了,常见原因是数据目录权限不对、postgresql.conf 里参数写错、或磁盘空间占满。
排查顺序是固定的:先看数据目录下的 postgresql-*.log 日志文件,时间戳对得上启动失败时间的就是根因;再用 pg_ctl 带 -D 数据目录前台启动,错误会直接打到控制台;确认服务端健康之前,不要调客户端超时参数。我在现场见过最冤的一次排查,同事花了半天调 Npgsql 超时,最后发现是数据库服务器磁盘满了,日志第一行就写着 "could not write to file: No space left on device"。
5. 把连接用稳:COPY 批量导入、Dapper 封装与参数化边界
5.1 NpgsqlBinaryImporter:用 COPY 协议批量导数据
数据初始化或同步场景,几万行起步的数据如果一条条 INSERT,再快的连接池也救不了。Npgsql 3.x 提供了 BeginBinaryImport,直接走 PostgreSQL 的 COPY 二进制协议,批量插入速度能比逐条 INSERT 快一个数量级。connString 就用 3.1 节那一串,最小用法如下:
using (var conn = new NpgsqlConnection(connString)) { conn.Open(); using (var writer = conn.BeginBinaryImport( "COPY import_tmp (id, name, amount) FROM STDIN WITH (FORMAT BINARY)")) { for (var i = 0; i < 100000; i++) { writer.StartRow(); writer.Write(i, NpgsqlDbType.Integer); // int4 writer.Write("name_" + i, NpgsqlDbType.Text); // text writer.Write(i * 1.5m, NpgsqlDbType.Numeric); // numeric } writer.Complete(); // 提交这批数据 } }逻辑说明:StartRow 开始一行,Write 按目标表列的顺序写入,带 NpgsqlDbType 的重载避免类型歧义。如果循环里出异常,不要调用 Complete,直接 Dispose 即中止本次 COPY,服务端会丢弃这批数据。为了重跑安全,我一般先 COPY 到临时表 import_tmp,全部成功后再 INSERT INTO 正式表 SELECT,失败就 DROP,不给脏数据留机会。注意 COPY 的列顺序必须和 SQL 里写的一致,少一列或换顺序都在服务端报错,这个报错时间点比较晚,出错时只能逐列对照。
5.2 Dapper + Npgsql:最小封装与参数传递
如果不想在业务代码里反复写 NpgsqlCommand,Dapper 是 .NET Framework 4.5 项目最省事的封装层。它和 Npgsql 的配合没有额外配置,只要连接串和 provider 正确:
using (var conn = new NpgsqlConnection(builder.ConnectionString)) { var users = conn.Query<User>( "SELECT id, name FROM users WHERE created_at > @since ORDER BY id LIMIT @limit;", new { since = DateTime.UtcNow.AddDays(-7), limit = 100 }).ToList(); }Dapper 会把匿名对象的属性名转成参数名,和 Npgsql 的命名参数对齐。这里有一个组合坑:如果一条 SQL 里同一个参数名出现两次,部分老版本 Dapper 加 Npgsql 3.x 会报 parameter not found,稳妥做法是拆成两个参数名(@since1 和 @since2)在代码里赋同样的值。另一个坑是匿名对象掉进了"看似参数化其实是拼串"的陷阱:Dapper 的 Query 重载如果第二个参数传的是字符串,会被当作命令文本的一部分处理,务必传对象而不是字符串。
5.3 参数化、事务与 EF6 的历史包袱
参数化的核心是显式类型。AddWithValue 能省代码,但遇到 numeric 列和 decimal 精度、timestamp 和 timestamptz 这种敏感类型时,推断结果不一定符合预期。显式写法是:
cmd.Parameters.Add(new NpgsqlParameter("amount", NpgsqlDbType.Numeric) { Value = 1234.56m });事务方面,老项目里最常见的错误是忘记给命令对象赋值事务:
using (var tx = conn.BeginTransaction()) { using (var cmd = new NpgsqlCommand( "UPDATE account SET balance = balance - @amount WHERE id = @id;", conn, tx)) { cmd.Parameters.AddWithValue("amount", 100m); cmd.Parameters.AddWithValue("id", 42); cmd.ExecuteNonQuery(); } tx.Commit(); }NpgsqlCommand 的构造函数第三参数接收事务,漏了它命令会在无事务状态下执行,异常时不回滚,排查时特别隐蔽。
如果你在 .NET Framework 4.5 项目里还挂着 EF6,配套的 provider 是 EntityFramework6.Npgsql,版本要跟上 Npgsql 3.2.7,装完必须检查 app.config 里 DbProviderFactories 有没有注册成功,否则运行时报 "The provider is not registered"。说实话,EF6 加 PostgreSQL 的历史包袱不小,新代码我一般劝退,直接 Dapper 或裸 ADO.NET 反而清爽。与 MySQL 的差异也提醒一句:PostgreSQL 的占位符不区分 @ 和 :,不支持 MySQL 的反引号,分页用 LIMIT / OFFSET,自增主键用 serial 或 identity,迁移 SQL 时别只改连接串。
6. 把版本号和连接状态写进日志:一个能救命的检查习惯
这个技巧不复杂,但救过我两次。启动时打印 Npgsql 程序集版本和服务端版本,排障先看这两行:
public static void LogConnectionInfo(string connString) { var asm = typeof(NpgsqlConnection).Assembly; Console.WriteLine("Npgsql Assembly: " + asm.FullName); using (var conn = new NpgsqlConnection(connString)) { conn.Open(); using (var cmd = new NpgsqlCommand("SELECT version();", conn)) { Console.WriteLine("PG Server: " + cmd.ExecuteScalar()); } } }把程序集名、程序集版本、server_version 三个输出打进日志头,再顺手查一下 pg_stat_activity 里当前连接的 state。我吃过一次亏:线上问题查了两天,最后发现 bin 里躺着一个 2.x 的 Npgsql.dll,日志第一行如果当时就打印了版本,一眼就能看穿。后来我把这个方法塞进连接工厂,每次升级 Npgsql、迁移服务器、切换 PostgreSQL 大版本前先跑一遍,确认客户端和服务端都在预期范围内再看业务。这个习惯的成本是一次启动日志的输出,收益是省掉无数个莫名其妙的"连不上"。希望帮到你。
本文还有配套的精品资源,点击获取