【EF Core】使用自定义的值比较器
举一个连外星人都知道的例子。假设有这样的实体类。public class Company { public Guid CompID { get; set; } public required string Name { get; set; } public required string Phone { get; set; } }这个类表示某些公司信息除主键外两个属性分别表示公司名称和固话。银河系居民都了解固话由区号和电话号码组成且有两种写法(010)88988989 010-88988989也就是说用户给 Phone 属性设置这两个值指的是同一个固话。若以默认的字符串比较器肯定会认为二者不相等的。所以这就要咱们动手了。要实现自定义的比较器99% 的做法是从 ValueComparerT 类派生。不需要重写任何成员只提供三个方法由对应的委托类型接收的实现然后传给基类的构造函数即可。需要的三个委托为1、FuncT, T, bool两个输入参数是 T 的值返回值是 bool 类型。该委托用于判断两个值是否相等相等就返回 true不相等就返回 false。2、FuncT, int返回输入参数 T 的哈希值整型。3、FuncT, T此委托用于创建“快照”由 ChangeTracker 负责管理。返回的 T 实例就是创建的快照实例。对于简单类型咱们不需要实现这个委托。它主要面向需要深度拷贝或存在嵌套数据的值用于自定义属性值的拷贝。对于这个固定电话咱们要实现相等比较和哈希计算创建快照不需要实现使用 EF Core 内置的就可以。于是定义 MyValueComparer 类。public class MyValueComparer : ValueComparerstring { /// summary /// 匹配规则 /// /summary private const string REGEX_PATTERN ^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$; #region 辅助方法 /// summary /// 分析固话号码 /// /summary /// param nameno输入值/param /// returns返回区号和电话/returns protected static (string code, string phone) ParsePhoneNo(string no) { // 分析号码 var res Regex.Match(no, REGEX_PATTERN); // 实际捕捉两个分组 if(res.Success) { return ( res.Groups[1].Value, res.Groups[2].Value ); } return (string.Empty, string.Empty); } /// summary /// 两个固话号码是否相等 /// /summary protected static bool IsEqual(string? val1, string? val2) { if (val1 null val2 null) return true; if (val1 null val2 ! null) return false; if (val1 ! null val2 null) return false; string code1, phone1; // 第一个号码 string code2, phone2; // 第二个号码 (code1, phone1) ParsePhoneNo(val1); (code2, phone2) ParsePhoneNo(val2); // 两个号码的区号与电话是否相同 return (code1 code2 phone1 phone2); } /// summary /// 计算哈希值 /// /summary protected static int GetHash(string val) { (string code, string ph) ParsePhoneNo(val); return HashCode.Combine(code, ph); } #endregion public MyValueComparer() :base((p1, p2) IsEqual(p1, p2), p GetHash(p)) { // ......... } }上面代码中老周使用正则表达式来提取固话中的区号的号码。先解释一下这个匹配规则。^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$^ 表示开头字符\(? 表示开头的字符可能是左括号但可能不出现所以用 ? 匹配(\d{3,4}) 这是一个分组匹配时会把它存储起来\d 是数字{34} 表示数字的出现次数为最少三次最多四次。即区号由三到四个数字组成(?:\)|-)? 表示可选的右括号或者“-”。(?: ...) 避免被识别为分组因为我们对右括号和“-”不感兴趣匹配结果也不要存储这些字符所以用 ?: 告诉正则处理引擎可以匹配它但不要存到结果中我们不需要“|”表示分支并列、或即可以出现右括号或“-”。后面的 ? 表示这个分组可以出现可以不出现。其实这里用“”也可以右括号和“-”应该至少出现一次(\d{6,})$ “$”表示字符串结尾号码部分同样也是匹配数字{6, } 表示至少出现六次。也可以是 {6,8}不过这里老周就没把规则定那么严格。正则匹配成功后应从 Groups 集合中找结果不要去 Captures 中找。Groups 集合存储了两个分组中匹配的数字字符区号和号码。if(res.Success) { return ( res.Groups[1].Value, res.Groups[2].Value ); }Groups 集合中第一个元素存的是整个字符串所以要从第二个元素获取分组的文本索引1起。好了现在咱们实现数据库上下文类并在配置数据库模型时应用自定义的比较器。public class MyContext : DbContext { public DbSetCompany Companies { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(server.\\Test;databasesome_db;Trust Server CertificateTrue;Integrated SecurityTrue); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityCompany(ent { ent.Property(x x.CompID).HasColumnName(cmp_id); ent.Property(x x.Name).HasColumnName(cmp_name).HasMaxLength(45); ent.Property(x x.Phone).HasColumnName(cmp_phone).HasMaxLength(15).Metadata.SetValueComparer(newMyValueComparer());ent.HasKey(x x.CompID).HasName(PK_Company); }); } }要注意的是PropertyBuilder 的成员方法/扩展方法并没有让咱们设置比较器的使用 HasConversion 方法除外配置值转换器时可以设置比较器。但这种只适合你需要转换类型的情形。不过没事咱们可以通过元数据对象来设置。.Metadata.SetValueComparer(new MyValueComparer())访问 PropertyBuilder.Metadata 得到的是属性元数据。要是访问 EntityTypeBuilder 的 Metadata 呢那就是实体类型元数据。下面创建数据库上下文实例先动态创建数据库然后我们修改第一条记录的 Phone 属性为【与原电话号码相同但格式不同的电话】。using (var c new MyContext()) { var created c.Database.EnsureCreated(); if(created) { c.Companies.AddRange([ new Company { Name 三鬼贸易有限公司, Phone 020-55128130 }, new Company { Name 一口焖信息服务有限公司, Phone 0765-20881919 } ]); c.SaveChanges(); } } using(var c new MyContext()) { var obj c.Companies.FirstOrDefault(); if(obj ! null) { obj.Phone (020)55128130; c.ChangeTracker.DetectChanges(); // 检测是否更改 // 打印跟踪信息 Console.WriteLine(c.ChangeTracker.ToDebugString(ChangeTrackerDebugStringOptions.IncludeProperties)); } }在插入数据时咱们设置的值是 020-55128130之后我们修改为 (020)55128130咱们认为这是同一个号码。由于这里老周没有调用 SaveChanges 方法即不会保存到数据库。所以需要调用一次 ChangeTracker.DetectChanges 方法强制 context 做一轮更改扫描。最后向控制台打印跟踪信息。属性修改后跟踪信息如下Company {CompID: 286554a7-e1f4-4ab1-e791-08deae71e616}UnchangedCompID: 286554a7-e1f4-4ab1-e791-08deae71e616 PK Name: 三鬼贸易有限公司 Phone: (020)55128130 Originally 020-55128130抠亮眼睛看清楚呢属性值确实是变了的但由于咱们自定义的比较器在作怪所以实体的状态依然被标记为 Unchanged。-----------------------------------------------------------------------------------------------接下来咱们看看值转器和值比较器一起用的情况。// 表示颜色的结构 public struct RGBColor { public byte Red { get; set; } public byte Green { get; set; } public byte Blue { get; set; } // 构造函数 public RGBColor(byte r, byte g, byte b) { Red r; Green g; Blue b; } } // 纸张 - 实体类 public class Paper { public int ID { get; set; } public int WidthCM { get; set; } public int HeightCM { get; set; } publicRGBColorColor { get; set; } }Paper 实体类的 Color 属性是 RGBColor 结构类型而存入数据库时我们只需要一个 uint 值即可因此它需要一个值转换器。public class RGBValueConverter : ValueConverterRGBColor, uint { #region 封装方法 private static RGBColor IntToColor(uint color) { byte red Convert.ToByte((color 16) 0xff); byte green Convert.ToByte((color 8) 0xff); byte blue Convert.ToByte(color 0xff); returnnewRGBColor(red, green, blue); } private static uint ColorToInt(ref RGBColor color) { return Convert.ToUInt32((color.Red 16) | (color.Green 8) |color.Blue); } #endregion // 构造函数 public RGBValueConverter() : base(rgb ColorToInt(ref rgb), cv IntToColor(cv)) { } }由于 uint 是 32 位整数咱们用它的低 24 位就可以表示 RGB 值。在查询数据时数据库提供程序先以 uint 类型读出值然后转为 RGBColor 结构实例再赋给 Paper.Color 属性反过来存入数据时将 RGBColor 的三个属性合成一个 uint 值再用此值写入数据库。由于 Paper 实体类的 Color 属性用的 RGBColor 类型所以比较器应面向 RGBColor 结构。public class RGBValueComparer : ValueComparerRGBColor { #region 封装的方法 // 相等比较 private static bool ColorEqual(RGBColor c1, RGBColor c2) { return c1.Red c2.Red c1.Green c2.Green c1.Blue c2.Blue; } // 获取哈希值 private static int ColorHash(RGBColor c) { HashCode hc new(); hc.Add(c.Red); hc.Add(c.Green); hc.Add(c.Blue); return hc.ToHashCode(); } // 创建快照 private static RGBColor CreateSnapshot(RGBColor c) {

相关新闻

本地部署DeepSeek大模型:vLLM+AWQ实战指南

本地部署DeepSeek大模型:vLLM+AWQ实战指南

1. 项目概述:为什么要在本地跑 DeepSeek,而不是只用网页版?“How to Run DeepSeek Locally: A Step-by-Step Guide”这个标题一出来,我就知道很多人点进来不是为了学命令行,而是想搞清楚一件事:我到底值不值…

2026/6/26 5:47:48阅读更多 →
从清华学霸到AI布道者,祝雪娇的下一个战场在哪里?

从清华学霸到AI布道者,祝雪娇的下一个战场在哪里?

在人工智能的浪潮里,祝雪娇绝对是个“狠角色”。这位1986年出生的清华学霸,凭着对技术的痴迷和对未来的敏锐嗅觉,从传统互联网跨界而来,在AI应用赛道一路“狂飙”,至今依然站在行业的最前沿。他的创业之路就像坐过山车…

2026/6/26 5:42:47阅读更多 →
掌上高考——高校数据爬取+数据可视化

掌上高考——高校数据爬取+数据可视化

一、选题的背景 选择此选题是因为掌上高考是一个提供本科院校信息的网站,通过爬取该网站的数据,可以获取到各个本科院校的相关信息,如学校名称、所在地、专业设置等。通过对这些数据进行分析和可视化,可以帮助学生更好地了解各个…

2026/6/26 5:42:47阅读更多 →
openYuanrong frontend:云原生函数网关的终极解决方案 [特殊字符]

openYuanrong frontend:云原生函数网关的终极解决方案 [特殊字符]

openYuanrong frontend:云原生函数网关的终极解决方案 🚀 【免费下载链接】yuanrong-frontend openYuanrong frontend:openYuanrong 网关,支持函数创建、调用等功能 项目地址: https://gitcode.com/openeuler/yuanrong-frontend…

2026/6/26 7:12:54阅读更多 →
从寄存器角度理解 Type-C 上电与下电:两种控制方式解析

从寄存器角度理解 Type-C 上电与下电:两种控制方式解析

1. 项目背景在嵌入式 Linux 开发中,很多外设并不是系统启动后就一直保持供电。例如 USB Type-C 接口、外部模组、电源芯片、通信模块等,通常会通过一个电源使能引脚进行控制。这个使能引脚一般由 GPIO 控制。当 GPIO 输出高电平时,电源开关芯…

2026/6/26 7:12:54阅读更多 →
Java基础:String、StringBuilder 和 StringBufferr对比

Java基础:String、StringBuilder 和 StringBufferr对比

目录 基础用法 1.String 2.StringBuilder和StringBufferr 略微深入 1.为什么StringBuiler线程不安全 2.为什么StringBuffer线程安全 基础用法 1.String 在Java中,String是不可变类。 所以new一个String对象之后,它的值是不可变的。对它的修改&a…

2026/6/26 7:12:54阅读更多 →
电磁流量计选型指南:精准匹配工况需求,保障工业测量可靠性

电磁流量计选型指南:精准匹配工况需求,保障工业测量可靠性

引言:工业测量基石的选型挑战 在现代工业自动化与智能化浪潮中,过程控制仪表作为感知系统的关键组成部分,其性能直接决定了生产流程的安全性、效率和产品质量。其中,电磁流量计凭借无机械运动部件、测量精度高、适用介质广泛等优势…

2026/6/26 7:12:54阅读更多 →
数仓建模理论

数仓建模理论

因为工作原因,小黄需要涉入大数据这一块的工作,所以再次补习一下数仓建模这一块的理论,参考《阿里大数据之路》这本书,以及AI来给我讲解的方式进行学习。 什么是数仓建模 我觉得是这样,数仓整套工作分为数据存储和数据…

2026/6/26 7:12:54阅读更多 →
阿里云Linux云服务器部署Python项目:从零到上线的完整实战指南

阿里云Linux云服务器部署Python项目:从零到上线的完整实战指南

一、部署前的准备:选型与规划 在开始部署之前,需要做好充分的准备工作。这包括选择合适的云服务器配置、规划网络与安全策略,以及准备本地开发环境。良好的前期规划能够避免后续部署过程中的许多麻烦。 1.1 选择阿里云ECS实例 阿里云ECS&a…

2026/6/26 7:07:53阅读更多 →
【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体 一文搞定到底什么是智能体【人工智能】一文搞定到底什么是智能体一. LM,WorkFlow,Agent分别有什么么不同二. Agent的思考过程是怎样的三. Agent的五个核心部分1)LLM2)Prompt3)Me…

2026/6/25 9:39:54阅读更多 →
嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

1. 嵌入式GUI控件:从原理到实战的深度解析在嵌入式系统开发中,图形用户界面(GUI)的设计与实现往往是项目从“能用”到“好用”的关键一跃。不同于资源充沛的PC或移动平台,嵌入式设备的GUI需要在有限的CPU性能、内存空间…

2026/6/26 4:15:25阅读更多 →
Google AI Studio 300美元额度的真相与实战指南

Google AI Studio 300美元额度的真相与实战指南

1. 这300美金不是“送钱”,而是Google埋下的第一道技术门槛 你看到标题里那个醒目的“$300美金”时,第一反应可能是:又一个免费额度?领完就完事?我亲手试过——这300美金根本不是红包,而是一张入场券&…

2026/6/25 9:01:34阅读更多 →
HPE (慧与) 服务器专用 ESXi 9 全套官方定制资源详解 + 完整部署升级教程

HPE (慧与) 服务器专用 ESXi 9 全套官方定制资源详解 + 完整部署升级教程

一、前言:企业运维痛点与资源价值自博通收购 VMware 之后,原 VMware 公开免费下载渠道全面关闭,企业运维人员想要获取适配 HPE 慧与服务器的 ESXi 9 原厂镜像,必须注册博通账号、绑定有效授权才能下载,无授权账号无法获…

2026/6/26 0:02:15阅读更多 →
Kotlin的@JvmStatic与@JvmField:与Java互操作的注解

Kotlin的@JvmStatic与@JvmField:与Java互操作的注解

Kotlin作为一门现代编程语言,与Java的互操作性一直是其核心优势之一。为了让Kotlin代码能够无缝对接Java,Kotlin提供了多种注解来优化互操作体验,其中JvmStatic和JvmField是两个关键注解。它们分别用于解决静态成员和字段在Java中的访问问题&…

2026/6/26 0:02:15阅读更多 →
深入解析musl libc中的mmap实现源码

深入解析musl libc中的mmap实现源码

最近在阅读musl libc源码时,发现其mmap的实现非常精妙,特分享给大家。 一、代码整体结构 这段代码实现了__mmap函数,并通过weak_alias导出为mmap。这是典型的musl libc风格——提供弱符号以便用户可以重写。 weak_alias(__mmap, mmap); 二…

2026/6/26 0:02:15阅读更多 →