You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

正则表达式特性在集成测试中未抛出异常的问题排查

问题:EF Core实体的正则验证未生效,纯空格输入通过了集成测试

我在做集成测试时遇到一个问题:我的ManualClassifier实体定义了RegularExpression数据注解,但当我给FirstName传入纯空格时,测试没有失败,数据反而被成功插入到数据库里了。

实体类代码

[Table("ManualClassifiers")] 
public class ManualClassifier : Classifier
{
    [RegularExpression(@"^([a-zA-Z]+?)([-\s'][a-zA-Z]+)*?")]
    [Required(ErrorMessage = "First name field is mandatory")]
    [Column(name: "first_name")]
    public string FirstName { get; set; }
    [RegularExpression(@"^([a-zA-Z]+?)([-\s'][a-zA-Z]+)*?")]
    [Required(ErrorMessage = "Last name field is mandatory")]
    [Column(name: "last_name")]
    public string LastName { get; set; }
    [EmailAddress]
    [Required(ErrorMessage = "Email field is mandatory")]
    [Column(name: "email")]
    public string Email { get; set; }
    [RegularExpression(@"^[a-zA-Z0-9._]+$")]
    [Required(ErrorMessage = "Username field is mandatory")]
    [Column(name: "username")]
    public string Username { get; set; }
    [Required(ErrorMessage = "Password hash field is mandatory")]
    [Column(name: "passwordHash")]
    public string PasswordHash { get; set; }
    [Required(ErrorMessage = "Password salt field is mandatory")]
    [Column(name: "passwordSalt")]
    public string PasswordSalt { get; set; }
}

测试代码

[TestMethod]
public async Task Insert_ManualClassifier_With_Empty_Field_Fails()
{
    using var connection = new SqliteConnection("DataSource=:memory:");
    connection.Open();

    var options = new DbContextOptionsBuilder<DataContext>()
        .UseSqlite(connection)
        .Options;

    using (var context = new DataContext(options))
    {
        await context.Database.EnsureCreatedAsync();
    }

    using (var context = new DataContext(options))
    {
        var user = new ManualClassifier()
        {
            FirstName = "   ", // 此处应为无效输入,测试应失败但实际通过
            LastName = "Last",
            Email = "example@gmail.com",
            Username = "firstlast123"
        };

        IRepositoryService repoService = new RepositoryService(context, NullLogger<RepositoryService>.Instance);

        string passwordHash = "pwhash";
        string passwordSalt = "pwsalt";

        int userID = await repoService.CreateUser(user, passwordHash, passwordSalt);
    }
}

仓库方法代码

public async Task<int> CreateUser(ManualClassifier user, string passwordHash, string passwordSalt)
{
    if (_context.ManualClassifiers.Any(u => u.Username == user.Username))
    {
        _logger.LogDebug($"User with username {user.Username} already exists");
        throw new UsernameAlreadyExistsException(user.Username);
    }

    if (_context.ManualClassifiers.Any(u => u.Email == user.Email))
    {
        _logger.LogDebug($"User with email {user.Email} already exists");
        throw new EmailAlreadyExistsException(user.Email);
    }

    user.PasswordHash = passwordHash;
    user.PasswordSalt = passwordSalt;
    user.Email = user.Email.ToLower();

    _logger.LogDebug("New user created successfully");
    await _context.ManualClassifiers.AddAsync(user);
    await _context.SaveChangesAsync();

    return user.Id;
}

原因分析

  1. EF Core不自动执行数据注解验证:RegularExpression、Required这类数据注解,EF Core本身不会在SaveChangesAsync时触发验证逻辑。这些注解主要用于ASP.NET MVC/Web API的模型绑定阶段,或者需要手动调用验证API检查实体有效性。
  2. 正则表达式存在缺陷:当前正则^([a-zA-Z]+?)([-\s'][a-zA-Z]+)*?没有以$结尾,意味着只要字符串开头匹配规则就会通过(不过本次问题的核心是验证根本未执行)。

解决方案

方案1:在仓库方法中手动验证实体

在保存前使用Validator类手动验证实体,验证失败则抛出异常:

using System.ComponentModel.DataAnnotations;

public async Task<int> CreateUser(ManualClassifier user, string passwordHash, string passwordSalt)
{
    // 新增实体验证逻辑
    var validationContext = new ValidationContext(user);
    var validationResults = new List<ValidationResult>();
    if (!Validator.TryValidateObject(user, validationContext, validationResults, validateAllProperties: true))
    {
        var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage));
        throw new InvalidOperationException($"Invalid user data: {errors}");
    }

    // 原有重复检查逻辑
    if (_context.ManualClassifiers.Any(u => u.Username == user.Username))
    {
        _logger.LogDebug($"User with username {user.Username} already exists");
        throw new UsernameAlreadyExistsException(user.Username);
    }

    if (_context.ManualClassifiers.Any(u => u.Email == user.Email))
    {
        _logger.LogDebug($"User with email {user.Email} already exists");
        throw new EmailAlreadyExistsException(user.Email);
    }

    user.PasswordHash = passwordHash;
    user.PasswordSalt = passwordSalt;
    user.Email = user.Email.ToLower();

    _logger.LogDebug("New user created successfully");
    await _context.ManualClassifiers.AddAsync(user);
    await _context.SaveChangesAsync();

    return user.Id;
}

方案2:在DbContext中全局验证

重写DbContext的SaveChangesAsync方法,统一验证所有新增/修改的实体:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    var entities = ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
        .Select(e => e.Entity);

    foreach (var entity in entities)
    {
        var validationContext = new ValidationContext(entity);
        Validator.ValidateObject(entity, validationContext, validateAllProperties: true);
    }

    return await base.SaveChangesAsync(cancellationToken);
}

方案3:修复正则表达式

给正则加上$结尾,确保整个字符串符合规则,同时避免纯空格输入:

// 修改后的正则,限制整个字符串必须匹配规则
[RegularExpression(@"^[a-zA-Z]+([-\s'][a-zA-Z]+)*$")]

内容的提问来源于stack exchange,提问作者Miguel Ferreira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 23:40:50