1. EF6 写 localdb 中文变问号问题到底出在哪一层如果你在用 EF6 配合 localdb 做本地开发遇到一个很典型的现象直接在 SSMS 或 VS 的 SQL 查询窗口里insert into一条中文数据查出来是正常的但换成 ASP.NET 里用 EF6 的SaveChanges()写进去中文就全变成问号???。这个现象我第一次碰到时也懵了很久因为同一张表、同一个字段SQL 手写没问题代码写就乱码直觉上会怀疑是 C# 字符串编码问题但实际排查下来根子往往在数据库的排序规则Collation上。localdb 是 SQL Server Express 的一个轻量运行模式默认实例创建时用的排序规则通常是SQL_Latin1_General_CP1_CI_AS。这个排序规则对应的代码页是 Latin1对非 Unicode 字符类型varchar、char、text来说它无法正确存储中文写入时会被替换成问号。而你在查询窗口里手写 SQL 能显示正常很可能是因为你写的是N测试这种 Unicode 字面量或者查询工具做了额外处理掩盖了底层存储已经损坏的事实。EF6 默认把string映射成nvarchar理论上应该走 Unicode 通道不受排序规则影响。但问题在于如果数据库或列的排序规则是 Latin1而 EF6 在生成参数时用了非 Unicode 的SqlDbType.VarChar或者迁移脚本里字段被定义成了varchar中文就会在写入那一刻被转成问号。所以这条链路要分三层看连接字符串有没有指定字符集相关参数、数据库和列的排序规则是不是中文、Code First 迁移生成的字段类型对不对。适合读这篇的人正在用 EF6 localdb 做本地开发、被中文乱码卡住、想按步骤定位到底哪一层出问题的同学。下面我会给出可复制的App.config/Web.config骨架、一段最小插入验证代码以及逐步验证动作帮你把乱码发生的位置钉死。2. 前置准备TaoToken 与 localdb 环境确认在动手改配置之前先把环境确认清楚。localdb 的实例名一般是(localdb)\MSSQLLocalDB你可以用sqllocaldb info命令查看当前实例状态。如果你在排查过程中需要对照 EF6 的官方行为、或者想快速验证某段 SQL 在 Unicode 下的表现可以借助 TaoToken 的模型对话能力来辅助理解报错和生成排查脚本它支持多模型切换适合边查边试。TaoToken 的接入入口在这里官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址https://taotoken.net/api模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite需要说明的是TaoToken 在这里的角色是帮你查资料、验证思路、生成排查 SQL 的辅助工具不是替代你本地数据库操作的东西。localdb 的排序规则修改、EF6 的配置调整还是要在你自己的开发环境里完成。确认环境时重点看三件事当前 localdb 实例的排序规则是什么、你的 EF6 上下文连的是哪个数据库、Code First 迁移生成的字段类型是nvarchar还是varchar。这三件事决定了乱码发生在哪一层。3. 可复制配置App.config / Web.config 骨架与排序规则修正3.1 连接字符串骨架EF6 的连接字符串里providerName必须是System.Data.SqlClientAttachDbFilename指向你的 mdf 文件。很多人乱码的根源之一是连接字符串里加了Charset之类的参数但 SqlClient 并不认这个参数真正影响字符存储的是数据库排序规则不是连接字符串。下面是一个可用的骨架configuration configSections section nameentityFramework typeSystem.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version6.0.0.0, Cultureneutral, PublicKeyTokenb77a5c561934e089 requirePermissionfalse / /configSections connectionStrings add nameMyDbContext connectionStringData Source(localdb)\MSSQLLocalDB;Initial CatalogMyDb;Integrated SecurityTrue;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings entityFramework defaultConnectionFactory typeSystem.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework parameters parameter valuemssqllocaldb / /parameters /defaultConnectionFactory providers provider invariantNameSystem.Data.SqlClient typeSystem.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer / /providers /entityFramework /configuration注意这里没有加任何Charset参数因为 SqlClient 的字符处理由数据库排序规则和字段类型决定。如果你看到网上有人让你在连接字符串里加Charsetutf8那是 MySQL 的写法放到 SqlClient 上无效反而会误导排查方向。3.2 修改 localdb 数据库排序规则localdb 默认排序规则不是中文这是乱码的核心原因。你需要把数据库排序规则改成Chinese_PRC_CI_AS或Chinese_PRC_CS_AS。改之前要先把数据库切到单用户模式否则会报「数据库正在使用」的错误。下面这段 SQL 可以直接在 SSMS 或 VS 的查询窗口里执行DECLARE database NVARCHAR(100) DECLARE sql NVARCHAR(500) DECLARE tmpCur CURSOR FOR SELECT DB_NAME() OPEN tmpCur FETCH NEXT FROM tmpCur INTO database SELECT sql ALTER DATABASE [ database ] SET SINGLE_USER WITH ROLLBACK IMMEDIATE EXEC(sql) SELECT sql ALTER DATABASE [ database ] COLLATE Chinese_PRC_CI_AS EXEC(sql) SELECT sql ALTER DATABASE [ database ] SET MULTI_USER EXEC(sql) CLOSE tmpCur DEALLOCATE tmpCur执行完之后用SELECT DATABASEPROPERTYEX(MyDb, Collation)确认排序规则已经变成Chinese_PRC_CI_AS。这一步只改数据库级排序规则已有的列如果之前是varchar还需要单独改列或者干脆重建表。3.3 Code First 迁移里字段类型要盯紧EF6 默认把string映射成nvarchar但如果你在OnModelCreating里用了HasColumnType(varchar)或者迁移脚本被手动改过字段就会变成varchar中文照样乱码。检查迁移文件里的CreateTable语句确认中文字段是nvarchar而不是varcharpublic partial class Init : DbMigration { public override void Up() { CreateTable( dbo.Articles, c new { Id c.Int(nullable: false, identity: true), Title c.String(maxLength: 200, unicode: true), Content c.String(unicode: true), }) .PrimaryKey(t t.Id); } }unicode: true是关键它会让 EF6 生成nvarchar。如果你之前已经生成了varchar列改完模型后重新生成迁移或者手动执行ALTER TABLE Articles ALTER COLUMN Title NVARCHAR(200)。4. 验证请求最小插入代码与成功结果配置改完之后用一段最小代码验证中文能不能正确写入。下面是一个控制台或 ASP.NET 里都能用的片段using (var db new MyDbContext()) { var article new Article { Title 中文测试标题, Content 这是一段中文内容用来验证 EF6 写入 localdb 是否乱码。 }; db.Articles.Add(article); db.SaveChanges(); var saved db.Articles.OrderByDescending(a a.Id).FirstOrDefault(); Console.WriteLine(写入后读回 saved.Title); Console.WriteLine(写入后读回 saved.Content); }运行后如果控制台输出的是正常中文说明整条链路已经通了。如果还是问号就继续往下看排查清单。你也可以在数据库里直接查SELECT TOP 5 Id, Title, Content FROM Articles ORDER BY Id DESC如果这里显示正常但 C# 读回来是问号那问题在读取端的编码处理如果这里就是问号那问题在写入端回到排序规则和字段类型上继续查。5. 本篇常见错排查清单5.1 改了数据库排序规则但列还是 varchar数据库级排序规则改了不代表已有列会自动变。varchar列的排序规则是列级属性需要单独改。用下面这段 SQL 查出所有非 Unicode 的中文字段SELECT t.name AS TableName, c.name AS ColumnName, c.max_length, c.collation_name FROM sys.columns c JOIN sys.tables t ON c.object_id t.object_id JOIN sys.types ty ON c.user_type_id ty.user_type_id WHERE ty.name IN (varchar, char, text) ORDER BY t.name, c.name查到之后逐个ALTER TABLE ... ALTER COLUMN ... NVARCHAR(...)。5.2 连接字符串里加了无效的 Charset 参数前面提过SqlClient 不认Charset。如果你从 MySQL 或别的数据库迁移过来习惯性加了Charsetutf8它不会报错但也不会生效反而让你以为已经处理了编码问题。删掉它把注意力放回排序规则和字段类型。5.3 迁移没重新生成模型改了但数据库没变Code First 里改了模型属性如果没有Add-Migration和Update-Database数据库结构不会变。很多人改了unicode: true却忘了更新迁移结果字段还是旧的varchar。用Update-Database -Verbose看实际执行的 SQL确认ALTER COLUMN有没有跑。5.4 localdb 实例被多个项目共用排序规则互相覆盖localdb 的(localdb)\MSSQLLocalDB是共享实例如果你有多个项目连同一个实例、同一个数据库名排序规则可能被别的项目改回去。排查时先确认SELECT DB_NAME()和DATABASEPROPERTYEX的结果别改错了库。5.5 读取端用了非 Unicode 的 DataReader极少数情况下写入是nvarchar但读取时用了GetString之外的GetSqlString或手动转varchar也会出现问号。检查你的查询有没有CAST或CONVERT到varchar有的话去掉。6. 继续排查与工具入口乱码排查的核心思路是分层定位先确认数据库排序规则再确认列类型最后确认 EF6 映射和读取方式。这三层里任何一层用了非 Unicode 通道中文就会在那一层被替换成问号。改完排序规则后记得重新生成迁移并更新数据库别只改一半。如果你在排查过程中需要快速验证某段 SQL 的行为、或者想让模型帮你分析 EF6 生成的 SQL 日志可以用 TaoToken 的模型对话来辅助模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite如果你在做长期的 EF6 项目维护、需要频繁生成迁移脚本和排查 SQL 日志可以考虑 Coding Plan把这类重复性的排查工作交给模型辅助Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite最后提醒一句改排序规则前先备份 mdf 文件localdb 虽然轻量但改坏了重建也麻烦。把Chinese_PRC_CI_AS设对、字段设成nvarchar、迁移更新到位EF6 写中文就不会再变问号了。
