【EF Core】利用自界说的值比力器

[复制链接]
发表于 2026-5-18 02:09:10 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
EF Core 默认实现了许多值比力器,用于在实体状态追踪时查抄属性值是否被修改。故大多环境下,咱们不必要操纵心。但,凡是总有特别环境,有些数据固然值不相称,但所表现的意思是相称的。这种时间就不能依赖默认的比力器了。
老周举一个连外星人都知道的例子。假设有如许的实体类。
  1. public class Company
  2. {
  3.     public Guid CompID { get; set; }
  4.     public required string Name { get; set; }
  5.     public required string Phone { get; set; }
  6. }
复制代码
这个类表现某些公司信息,除主键外,两个属性分别表现公司名称和固话。银河系住民都相识,固话由区号和电话号码构成,且有两种写法:
  1. (010)88988989
  2. 010-88988989
复制代码
也就是说,用户给 Phone 属性设置这两个值,指的是同一个固话。若以默认的字符串比力器,肯定会以为二者不相称的。以是,这就要咱们动手了。
要实现自界说的比力器,99% 的做法是从 ValueComparer 类派生。不必要重写任何成员,只提供三个方法(由对应的委托范例吸取)的实现,然后传给基类的构造函数即可。
必要的三个委托为:
1、Func:两个输入参数是 T 的值,返回值是 bool 范例。该委托用于判断两个值是否相称,相称就返回 true,不相称就返回 false。
2、Func:返回输入参数 T 的哈希值,整型。
3、Func:此委托用于创建“快照”,由 ChangeTracker 负责管理。返回的 T 实例就是创建的快照实例。对于简单范例,咱们不必要实现这个委托。它重要面向必要深度拷贝或存在嵌套数据的值,用于自界说属性值的拷贝。
对于这个固定电话,咱们要实现相称比力和哈希盘算,创建快照不必要实现,利用 EF Core 内置的就可以。
于是,界说 MyValueComparer 类。
  1. public class MyValueComparer : ValueComparer<string>
  2. {
  3.     /// <summary>
  4.     /// 匹配规则
  5.     /// </summary>
  6.     private const string REGEX_PATTERN = "<strong>^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$</strong>";
  7.     #region 辅助方法
  8.     /// <summary>
  9.     /// 分析固话号码
  10.     /// </summary>
  11.     /// <param name="no">输入值</param>
  12.     /// <returns>返回区号和电话</returns>
  13.     protected static (string code, string phone) ParsePhoneNo(string no)
  14.     {
  15.         // 分析号码
  16.         var res = Regex.Match(no, REGEX_PATTERN);
  17.         // 实际捕捉两个分组
  18.         if(res.Success)
  19.         {
  20.             return (
  21.                     res.Groups[1].Value,
  22.                     res.Groups[2].Value
  23.                 );
  24.         }
  25.         return (string.Empty, string.Empty);
  26.     }
  27.     /// <summary>
  28.     /// 两个固话号码是否相等
  29.     /// </summary>
  30.     protected static bool IsEqual(string? val1, string? val2)
  31.     {
  32.         if (val1 == null && val2 == null)
  33.             return true;
  34.         if (val1 == null && val2 != null)
  35.             return false;
  36.         if (val1 != null && val2 == null)
  37.             return false;
  38.         string code1, phone1;       // 第一个号码
  39.         string code2, phone2;       // 第二个号码
  40.         (code1, phone1) = ParsePhoneNo(val1);
  41.         (code2, phone2) = ParsePhoneNo(val2);
  42.         // 两个号码的区号与电话是否相同
  43.         return (code1 == code2 && phone1 == phone2);
  44.     }
  45.     /// <summary>
  46.     /// 计算哈希值
  47.     /// </summary>
  48.     protected static int GetHash(string val)
  49.     {
  50.         (string code, string ph) = ParsePhoneNo(val);
  51.         return HashCode.Combine(code, ph);
  52.     }
  53.     #endregion
  54.     public MyValueComparer()
  55.            :base((p1, p2) => IsEqual(p1, p2),  p => GetHash(p))
  56.     {
  57.         // .........
  58.     }
  59. }
复制代码
上面代码中,老周利用正则表达式来提取固话中的区号的号码。先表明一下这个匹配规则。
  1. ^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$
复制代码

  • ^ 表现开头字符;
  • \(? 表现开头的字符大概是左括号,但大概不出现,以是用 ? 匹配;
  • (\d{3,4}) 这是一个分组,匹配时会把它存储起来,\d 是数字,{3,4} 表现数字的出现次数为最少三次最多四次。即区号由三到四个数字构成;
  • (?:\)|-)? 表现可选的右括号大概“-”。(?: ...) 克制被辨认为分组,由于我们对右括号和“-”不感爱好,匹配效果也不要存储这些字符,以是用 ?: 告诉正则处理处罚引擎可以匹配它,但不要存到效果中,我们不必要;“|”表现分支(并列、或),即可以出现右括号或“-”。反面的 ? 表现这个分组可以出现可以不出现。着实这里用“+”也可以,右括号和“-”应该至少出现一次;
  •  (\d{6,})$ “$”表现字符串末了,号码部门同样也是匹配数字,{6, } 表现至少出现六次。也可以是 {6,8},不外这里老周就没把规则定那么严酷。
正则匹配乐成后,应从 Groups 集会合找效果,不要去 Captures 中找。Groups 聚集存储了两个分组中匹配的数字字符(区号和号码)。
  1. if(res.Success)
  2. {
  3.     return (
  4.             res.Groups[1].Value,
  5.             res.Groups[2].Value
  6.         );
  7. }
复制代码
Groups 集会合,第一个元素存的是整个字符串,以是要从第二个元素获取分组的文本(索引1起)。
好了,如今咱们实现数据库上下文类,并在设置数据库模子时应用自界说的比力器。
  1. public class MyContext : DbContext
  2. {
  3.     public DbSet<Company> Companies { get; set; }
  4.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  5.     {
  6.         optionsBuilder.UseSqlServer(<em>"server=.\\Test;database=some_db;Trust Server Certificate=True;Integrated Security=True"</em>);
  7.     }
  8.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  9.     {
  10.         modelBuilder.Entity<Company>(ent =>
  11.         {
  12.             ent.Property(x => x.CompID).HasColumnName("cmp_id");
  13.             ent.Property(x => x.Name).HasColumnName("cmp_name").HasMaxLength(45);
  14.             ent.Property(x => x.Phone).HasColumnName("cmp_phone").HasMaxLength(15)
  15.                 <strong>.Metadata.SetValueComparer(</strong><strong>new</strong><strong> MyValueComparer());</strong>
  16.             ent.HasKey(x => x.CompID).HasName("PK_Company");
  17.         });
  18.     }
  19. }
复制代码
要注意的是,PropertyBuilder 的成员方法/扩展方法并没有让咱们设置比力器的(利用 HasConversion 方法除外,设置值转换器时可以设置比力器。但这种只得当你必要转换范例的环境)。不外没事,咱们可以通过元数据对象来设置。
  1. .Metadata.SetValueComparer(new MyValueComparer())
复制代码
访问 PropertyBuilder.Metadata 得到的是属性元数据。要是访问 EntityTypeBuilder 的 Metadata 呢,那就是实体范例元数据。
下面,创建数据库上下文实例,先动态创建数据库,然后我们修改第一条记载的 Phone 属性为【与原电话号码类似但格式差异的电话】。
  1. using (var c = new MyContext())
  2. {
  3.     var created = c.Database.EnsureCreated();
  4.     if(created)
  5.     {
  6.         c.Companies.AddRange([
  7.             new Company
  8.             {
  9.                 Name = "三鬼贸易有限公司",
  10.                 Phone = "020-55128130"
  11.             },
  12.             new Company
  13.             {
  14.                 Name = "一口焖信息服务有限公司",
  15.                 Phone = "0765-20881919"
  16.             }
  17.             ]);
  18.         c.SaveChanges();
  19.     }
  20. }
  21. using(var c = new MyContext())
  22. {
  23.     var obj = c.Companies.FirstOrDefault();
  24.     if(obj != null)
  25.     {
  26.         obj.Phone = "<strong>(020)55128130</strong>";
  27.         c.<em>ChangeTracker.DetectChanges</em>();    // 检测是否更改
  28.         // 打印跟踪信息
  29.         Console.WriteLine(c.ChangeTracker.ToDebugString(ChangeTrackerDebugStringOptions.IncludeProperties));
  30.     }
  31. }
复制代码
在插入数据时,咱们设置的值是 020-55128130,之后我们修改为 (020)55128130,咱们以为这是同一个号码。由于这里老周没有调用 SaveChanges 方法,即不会生存到数据库。以是,必要调用一次 ChangeTracker.DetectChanges 方法,欺压 context 做一轮更改扫描。末了向控制台打印跟踪信息。
属性修改后,跟踪信息如下:
  1. Company {CompID: 286554a7-e1f4-4ab1-e791-08deae71e616} <strong>Unchanged</strong>
  2.     CompID: '286554a7-e1f4-4ab1-e791-08deae71e616' PK
  3.     Name: '三鬼贸易有限公司'
  4.     Phone: '<strong>(020)55128130</strong>' Originally '<strong>020-55128130</strong>'
复制代码
抠亮眼睛看清晰呢,属性值确实是变了的,但由于咱们自界说的比力器在作怪,以是,实体的状态依然被标记为 Unchanged。
 -----------------------------------------------------------------------------------------------
接下来,咱们看看值转器和值比力器一起用的环境。
  1. // 表示颜色的结构
  2. public struct RGBColor
  3. {
  4.     public byte Red { get; set; }
  5.     public byte Green { get; set; }
  6.     public byte Blue { get; set; }
  7.     // 构造函数
  8.     public RGBColor(byte r, byte g, byte b)
  9.     {
  10.         Red = r;
  11.         Green = g;
  12.         Blue = b;
  13.     }
  14. }
  15. // 纸张 - 实体类
  16. public class Paper
  17. {
  18.     public int ID { get; set; }
  19.     public int WidthCM { get; set; }
  20.     public int HeightCM { get; set; }
  21.     public <strong>RGBColor</strong> Color { get; set; }
  22. }
复制代码
Paper 实体类的 Color 属性是 RGBColor 布局范例,而存入数据库时,我们只必要一个 uint 值即可,因此,它必要一个值转换器。
  1. public class RGBValueConverter : ValueConverter<RGBColor, uint>
  2. {
  3.     #region 封装方法
  4.     private static RGBColor IntToColor(uint color)
  5.     {
  6.         byte red = Convert.ToByte((color >> 16) & 0xff);
  7.         byte green = Convert.ToByte((color >> 8) & 0xff);
  8.         byte blue = Convert.ToByte(color & 0xff);
  9.         return <strong>new</strong><strong> RGBColor(red, green, blue)</strong>;
  10.     }
  11.     private static uint ColorToInt(ref RGBColor color)
  12.     {
  13.         return Convert.ToUInt32(<strong>(color.Red << 16) | (color.Green << 8) |</strong><strong> color.Blue</strong>);
  14.     }
  15.     #endregion
  16.     // 构造函数
  17.     public RGBValueConverter()
  18.         : base(rgb => ColorToInt(ref rgb), cv => IntToColor(cv))
  19.     { }
  20. }
复制代码
由于 uint 是 32 位整数,咱们用它的低 24 位就可以表现 RGB 值。在查询数据时,数据库提供步调先以 uint 范例读出值,然后转为 RGBColor 布局实例,再赋给 Paper.Color 属性;反过来,存入数据时,将 RGBColor 的三个属性合成一个 uint 值,再用此值写入数据库。
 
由于 Paper 实体类的 Color 属性用的 RGBColor 范例,以是,比力器应面向 RGBColor 布局。
  1. public class RGBValueComparer : ValueComparer<RGBColor>
  2. {
  3.     #region 封装的方法
  4.     // 相等比较
  5.     private static bool ColorEqual(RGBColor c1, RGBColor c2)
  6.     {
  7.         return c1.Red == c2.Red && c1.Green == c2.Green && c1.Blue == c2.Blue;
  8.     }
  9.     // 获取哈希值
  10.     private static int ColorHash(RGBColor c)
  11.     {
  12.         HashCode hc = new();
  13.         hc.Add(c.Red);
  14.         hc.Add(c.Green);
  15.         hc.Add(c.Blue);
  16.         return hc.ToHashCode();
  17.     }
  18.     // 创建快照
  19.     private static RGBColor CreateSnapshot(RGBColor c)
  20.     {
  21.         return new RGBColor(c.Red, c.Green, c.Blue);
  22.     }
  23.     #endregion
  24.     // 构造函数
  25.     public RGBValueComparer()
  26.         :base((c1, c2) => ColorEqual(c1, c2), c => ColorHash(c), c => CreateSnapshot(c))
  27.     { }
  28. }
复制代码
在用于创建快照的 CreateSnapshot 方法中,上述代码创了个 RGBColor 实例,并将原 RGBColor 对象的三个属性值作为参数传给构造函数。这就完成了深度拷贝。固然,由于 RGBColor 是值范例,着实它的实例本身就会把成员复制的。这里老周只演示功能,假如换成是类(引用),那就可以拷贝成员了,而不是仅通报引用。
在 ColorEqual 方法中,上述代码直接用比力两个 RGBColor 的实例的属性值。只有三个属性值都相称才行。
下面,咱们创建数据库上下文类,并做好设置。
  1. public class MyContext : DbContext
  2. {
  3.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  4.     {
  5.         // 配置连接字符串
  6.         SqlConnectionStringBuilder cnnsbd = new();
  7.         cnnsbd.DataSource = "(localdb)\\MSSQLLocalDB";
  8.         cnnsbd.InitialCatalog = "test_5";
  9.         cnnsbd.IntegratedSecurity = true;
  10.         optionsBuilder.UseSqlServer(cnnsbd.ConnectionString);
  11.     }
  12.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  13.     {
  14.         modelBuilder.Entity<Paper>(ent =>
  15.         {
  16.             ent.ToTable("tb_papers");
  17.             ent.Property(a => a.ID).HasColumnName("p_id");
  18.             ent.Property(a => a.WidthCM).HasColumnName("wid_cm");
  19.             ent.Property(a => a.HeightCM).HasColumnName("hei_cm");
  20.             ent.Property(a => a.Color)
  21.                  .HasColumnName("p_color")
  22.                  // 应用转换器
  23.                  .<strong>HasConversion(new RGBValueConverter(), new</strong><strong> RGBValueComparer())</strong>;
  24.             ent.HasKey(s => s.ID).HasName("PK_Papers");
  25.         });
  26.     }
  27.     // 数据集合
  28.     public DbSet<Paper> Papers { get; set; }
  29. }
复制代码
在设置 Color 属性时,通过 HasConversion 方法可以指定值转换器和值比力器。
创建数据库上下文实例,插入一条测试数据。
  1. using(var ctx = new MyContext())
  2. {
  3.     // 创建数据库
  4.     bool r = ctx.Database.EnsureCreated();
  5.     if(r)
  6.     {
  7.         // 插入测试数据
  8.         ctx.Papers.Add(new() { HeightCM = 219, WidthCM = 100, Color = new RGBColor(0x2f, 0x05, 0x66) });
  9.         ctx.SaveChanges();
  10.     }
  11. }
复制代码
把刚存入数据库的记载查询出来,然后咱们更换 Color 属性的值,再扫描一遍实体状态。
  1. using(var context = new MyContext())
  2. {
  3.     Paper? p = context.Papers.FirstOrDefault();
  4.     // 修改颜色
  5.     if(p is not null)
  6.     {
  7.         RGBColor c = p.Color;
  8.         c.Blue = 0x4e;
  9.         p.Color = c;
  10.         // 强制扫描更改
  11.         context.<strong>ChangeTracker.DetectChanges</strong>();
  12.         // 打印跟踪信息
  13.         Console.WriteLine(context.ChangeTracker.DebugView.LongView);
  14.     }
  15. }
复制代码
和上一个示例一样,由于老周没有调用 SaveChanges 方法,以是不会生存到数据库,也不会扫描实体是否更改。必要手动调用一次 DetectChanges 方法欺压举行一次扫描,末了打印实体跟踪信息。
输出效果如下:
  1. Paper {ID: 1} <strong>Modified</strong>
  2.     ID: 1 PK
  3.     Color: 'QixenLie.RGBColor' Modified Originally 'QixenLie.RGBColor'
  4.     HeightCM: 219
  5.     WidthCM: 100
复制代码
注意看,实体被标记为 Modified,表明通过值比力器已发现 Color 属性被更改。
 
好了,本日的水文就水到这里了。
 
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表