ASP.NET Core 项目写入 MySQL 时,varchar 字段的截取行为并不总是可预测。假设某张表的标题列定义为 varchar(50),接口收到 60 个字符的字符串,在开启严格模式的 MySQL 实例上会得到 Data too long for column 异常,接口返回 500;如果为了兼容旧数据关闭了严格模式,写入会成功,但尾部 10 个字符会被悄悄丢弃。对业务来说,这两种结果都不可接受。本文围绕该问题展开,先复现截断与报错条件,再介绍从实体配置、数据库层到代码层的完整处理方式。

varchar 截断是怎么发生的
MySQL 中的 varchar 长度定义的是字符数量,而不是字节数量。例如 varchar(50) 表示该列最多存储 50 个字符。但这个上限并不是绝对的,因为 MySQL 对整行存储大小和索引长度还有额外限制。常见误区是把 varchar(50) 当成 50 字节,然后按字节长度去切割字符串,结果在 utf8mb4 字符集下,中文和 Emoji 每个字符可能占 3 到 4 个字节,实际字符数量远少于预期。因此,理解 varchar 长度语义是处理截取问题的前提。
下面的建表语句定义了一个标题列,字符集为 utf8mb4:
CREATE TABLE article (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MySQL 的 sql_mode 对超长写入行为起决定性作用。默认情况下,从 MySQL 5.7 开始,严格模式 STRICT_TRANS_TABLES 在默认配置中就是开启的。严格模式下,如果插入到 title 列的字符串超过 50 个字符,数据库会直接返回错误,错误码为 1406,提示 Data too long for column。如果关闭严格模式,MySQL 只会产生一个 warning,并继续执行插入,超出的字符会被静默截断。可以先通过以下 SQL 查看当前会话和全局的 sql_mode:
SELECT @@SESSION.sql_mode; SELECT @@GLOBAL.sql_mode;
在 ASP.NET Core 项目中,如果使用 Entity Framework Core 写入数据,实体属性没有配置最大长度时,EF Core 自身并不会在应用层拦截超长字符串,而是直接生成 INSERT 语句发送给 MySQL。数据库驱动会把 MySQL 返回的 1406 错误包装成 MySqlException,再被 EF Core 包装为 DbUpdateException。这样一来,业务代码看到的异常类型并不直观,全局异常过滤器如果只按业务异常处理,就很容易把这种错误当成未知 500 错误返回给调用方。因此,仅仅依赖数据库报错或依赖驱动异常是不够的,必须在应用层提前收紧。
从实体映射和参数校验入手
最直接的方案是给实体属性加上 MaxLength 特性。这样 EF Core 在执行 SaveChanges 时可以在客户端进行校验,同时迁移工具也会根据该特性生成正确的 varchar 列定义。例如 Article 实体的 Title 属性可以这样配置:
using System.ComponentModel.DataAnnotations;
public class Article
{
public int Id { get; set; }
[MaxLength(50)]
public string Title { get; set; } = string.Empty;
}
如果项目采用 Fluent API 而不是 DataAnnotations,也可以在 DbContext 的 OnModelCreating 方法中调用 HasMaxLength。两种方式最终映射效果相同,但 Fluent API 的优势在于实体层可以保持干净,不依赖特定的数据注解库。示例如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Article>(entity =>
{
entity.Property(e => e.Title).HasMaxLength(50);
});
}
更推荐的做法是在 DTO 入口层就完成校验。ASP.NET Core 的 ApiController 特性会自动检查 ModelState,只要在请求模型上标记 MaxLength,超长请求会直接返回 400,而不会进入业务逻辑。这样做可以避免无效请求消耗数据库资源,也能给客户端更明确的语义。DTO 示例:
public class CreateArticleRequest
{
[Required]
[MaxLength(50)]
public string Title { get; set; } = string.Empty;
}
当然,DTO 校验可能被绕过。例如定时任务、消息队列消费者或直接调用服务层的代码,并不一定经过 Controller。此时 DbContext 层仍需兜底。一种做法是重写 SaveChanges,在保存前检查实体属性长度;另一种做法是在全局异常过滤器中捕获 DbUpdateException,识别 MySQL 的 1406 错误码并返回友好提示。下面代码展示了如何识别数据库层的超长异常:
try
{
await dbContext.SaveChangesAsync();
}
catch (DbUpdateException ex)
when (ex.InnerException is MySqlException mysqlEx && mysqlEx.Number == 1406)
{
return Results.BadRequest(new { message = "字段长度超出数据库限制" });
}
这种兜底策略虽然不能阻止异常发生,但至少能把错误转换为可读的业务响应,避免直接暴露 500。综合来看,实体映射负责数据库结构,DTO 校验负责接口入口,SaveChanges 或异常处理负责最后一道防线,三层配合才能把 varchar 截取问题控制在合理范围。
数据库列调整与安全截取策略
如果业务确实需要更长的字符串,光靠应用层校验并不能解决问题,应该调整数据库列定义。例如将标题列从 varchar(50) 改为 varchar(200),或者直接使用 TEXT 类型。修改列定义的 SQL 如下:
ALTER TABLE article MODIFY COLUMN title VARCHAR(200) NOT NULL;
但需要注意的是,MySQL InnoDB 对索引前缀长度有限制。使用 utf8mb4 字符集时,旧版本 MySQL 对 varchar 列创建索引时,最大前缀长度可能是 191 个字符。如果盲目把列增加到 255 并创建索引,很可能会遇到 Specified key was too long 错误。因此,在调整列长度时,要结合该列是否会参与索引来做决定。对于不需要索引的长文本字段,优先使用 TEXT;对于需要索引但内容确实较长的场景,可以使用前缀索引。
当业务要求必须按固定长度截取字符串时,不能简单调用 C# 的 string.Substring(0, 50)。C# 的 string 以 UTF-16 码元计数,表情符号和部分生僻字由一对代理项组成,直接截取可能会切出半个字符。这种非法字符串写入 MySQL 后可能显示为乱码,甚至导致后续处理异常。正确的做法是按文本元素截取,使用 StringInfo 枚举用户感知的字符:
using System.Globalization;
public static string TruncateByTextElements(string input, int maxLength)
{
if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
{
return input;
}
var enumerator = StringInfo.GetTextElementEnumerator(input);
var builder = new StringBuilder();
int count = 0;
while (enumerator.MoveNext() && count < maxLength)
{
string textElement = enumerator.GetTextElement();
builder.Append(textElement);
count++;
}
return builder.ToString();
}
该方法按用户看到的字符个数截取,可以正确处理中文、Emoji 以及组合字符。不过 MySQL 的 varchar 长度统计在大部分排序规则下也按字符数进行,与 StringInfo 的计数基本一致。如果仍需保证数据库层也安全,可以在 SQL 中使用 LEFT 函数截取,但更推荐在应用层完成截取,避免在 SQL 语句中引入隐式数据裁剪逻辑。SQL 中的 LEFT 用法如下:
INSERT INTO article (title) VALUES (LEFT(@input_title, 50));
需要明确的是,无论是应用层截取还是 SQL 层截取,本质上都是一种兜底方案。如果业务字段经常需要截断,说明字段长度设计本身可能不合理,应该优先重新评估数据模型,而不是长期依赖截断逻辑掩盖问题。
完整实例:ASP.NET Core + Pomelo EF Core
下面通过一个完整的 Web API 示例把上述方案串起来。首先创建 ASP.NET Core Web API 项目,并添加 Pomelo.EntityFrameworkCore.MySql 包。在 appsettings.json 中配置 MySQL 连接字符串,务必指定字符集为 utf8mb4,避免中文写入出现编码问题:
{
"ConnectionStrings": {
"DefaultConnection": "Server=127.0.0.1;Port=3306;Database=article_db;User=root;Password=123456;Character Set=utf8mb4;"
}
}
接着在 Program.cs 中注册 DbContext。这里使用 ServerVersion.AutoDetect 自动检测 MySQL 版本,适合本地开发和测试环境。生产环境建议显式指定服务器版本以提高稳定性:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection"))
));
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
定义 Article 实体和 AppDbContext。实体上的 MaxLength 特性会同时影响 EF Core 客户端校验和迁移生成,保证数据库结构与业务规则一致:
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
public class Article
{
public int Id { get; set; }
[MaxLength(50)]
public string Title { get; set; } = string.Empty;
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Article> Articles => Set<Article>();
}
最后编写 API 控制器。POST 接口接收 DTO,在服务层使用 StringInfo 安全截取,确保即使请求绕过 DTO 校验,标题长度也不会超过 50 个字符。保存成功后返回创建结果:
[ApiController]
[Route("api/articles")]
public class ArticlesController : ControllerBase
{
private readonly AppDbContext _db;
public ArticlesController(AppDbContext db)
{
_db = db;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateArticleRequest request)
{
var article = new Article
{
Title = TruncateByTextElements(request.Title, 50)
};
_db.Articles.Add(article);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(Create), new { id = article.Id }, article);
}
}
完成代码后执行 dotnet ef migrations add Init 以及 dotnet ef database update,即可生成对应数据库结构。向 /api/articles 发送一个标题长度超过 50 的请求,如果 DTO 上配置了 MaxLength,接口会直接返回 400;即使绕过 DTO 层,服务层也会把标题截取为 50 个字符后再保存。这样无论 MySQL 是否开启严格模式,都不会出现突然的 500 错误或静默丢字符问题。实际项目还可以结合全局异常处理和日志告警,进一步提升数据写入的稳健性。
ASP.NET CoreMySQL varchar字符串截取修改时间:2026-08-22 01:07:55