惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
博客园 - 叶小钗
P
Privacy International News Feed
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
IT之家
IT之家
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
罗磊的独立博客
I
Intezer
S
Schneier on Security
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

博客园 - xiangji

开源完美模块组件化可扩展的Xml解析器Hand.ParseXml Roslyn语法的模式匹配之EasySyntax增加模式匹配支持 SourceGenerator之扑风捉影 .NET源码生成器基于partial范式开发和nuget打包 SourceGenerator之partial范式及测试 .NET源码生成器之SyntaxTree踩坑 .NET源码生成器使用SyntaxTree生成代码及简化语法 DBShadow.net之依赖注入 DBShadow.net之化繁为简 DBShadow.net之性能优化的坎坷路 DBShadow横空出世,Dapper.net的天花板盖不住了 Aspire+.NET10+手搓线程池打造抓不死的云应用 鸡肋的TaskFactory是时候抛弃了 重构《手搓》TaskFactory带你更安全的起飞 异步"伪线程"重构《手搓》线程池,支持任务清退 《手搓》线程池优化的追求 《手搓》TaskFactory带你安全的起飞 《手搓》线程池 致敬1024,《手搓》轻量级EventBus PocoEmit遥遥领先于AutoMapper之打通充血模型的任督二脉 PocoEmit遥遥领先于AutoMapper之循环引用 微软.net表达式编译居然有bug? 如何使用PocoEmit.Mapper替代AutoMapper
开源项目PocoEmit.Mapper重构之扑风捉影
xiangji · 2026-04-21 · via 博客园 - xiangji

一、投影技术

  • 这里的投影可以实现类似sql投影的效果
  • 也可以类比光线照射在物体上形成的影像

1. 投影接口定义

  • IProjection接口很简单
  • 只有TryConvert一个方法
  • 相同类型对象的转化,返回是否成功以及转化后的结果
  • 约定不符合规则就不转化
  • 一般情况下转化成功的结果会和源对象不同
  • 当然经过多次投影后也可能会和源对象相同了
public interface IProjection<T>
{
    bool TryConvert(T source, out T result);
}

2. 三种投影方式

2.1 使用前缀投影来演示

  • 前缀投影就是把成员前面加个前缀来映射
  • sourceMembers模拟对User的反射
  • 通过Projection.Prefix("User")创建一个前缀投影
  • 如果没有User前缀,就增加User
  • 如果有User前缀就不处理
public record User(int Id, string UserName);
public record UserDTO(int UserId, string UserName);
var sourceMembers = new Dictionary<string, Func<User, object>>()
{
    [nameof(User.Id)] = obj => obj.Id,
    [nameof(User.UserName)] = obj => obj.UserName
};
var projection = Projection.Prefix("User");

2.2 Filter投影的Case

2.2.1 csharp代码
IDictionary<string, Func<User, object>> result = projection.Filter(sourceMembers);
Assert.Single(result);
Assert.True(result.ContainsKey(nameof(UserDTO.UserId)));
2.2.2 sql表示
SELECT Id AS UserId FROM User
2.2.3 影像表示

graph LR subgraph User Id UserName end Id -->|Filter| UserId

2.3 Through投影的Case

2.3.1 csharp代码
IDictionary<string, Func<User, object>> result = projection.Through(sourceMembers);
Assert.Equal(sourceMembers.Count, result.Count);
Assert.True(result.ContainsKey(nameof(UserDTO.UserId)));
Assert.True(result.ContainsKey(nameof(UserDTO.UserName)));
2.3.2 sql表示
SELECT Id AS UserId,UserName FROM User
2.3.3 影像表示

graph LR subgraph User User-Id[Id] User-UserName[UserName] end User-Id -->|Through| UserId User-UserName -->|Through| UserName

2.4 Cross投影的Case

  • PocoEmit.Mapper重构用的就是Cross投影
2.4.1 csharp代码
IDictionary<string, Func<User, object>> result = projection.Cross(sourceMembers);
Assert.Equal(3, result.Count);
Assert.True(result.ContainsKey(nameof(User.Id)));
Assert.True(result.ContainsKey(nameof(UserDTO.UserId)));
Assert.True(result.ContainsKey(nameof(UserDTO.UserName)));
2.4.2 sql表示
SELECT Id,UserName,Id AS UserId FROM User
2.4.3 影像表示

graph LR subgraph User User-Id[Id] User-UserName[UserName] end User-Id -->|Cross| Id User-UserName -->|Cross| UserName User-Id -->|Cross| UserId

3. 投影支持"横向"扩展

  • 这里说的"横向"就是投影并联
  • 把多个投影组合成多分支的映射规则
  • 通过FirstReturn或ToFirstReturn方法实现

3.1 "横向"扩展的Case

// 包含User或U前缀就去掉前缀
var user = Projection.RemovePrefix("User");
var u = Projection.RemovePrefix("U");
var projection = Projection.FirstReturn(user, u);

[Theory]
[InlineData("UserId", "Id")]
[InlineData("UId", "Id")]
[InlineData("UUserName", "UserName")]
[InlineData("UserUName", "UName")]
public void TryConvert(string source, string expected)
{
    projection.TryConvert(source, out var result);
    Assert.Equal(expected, result);
}

3.2 影像表示"横向"扩展

  • RemoveUser和RemoveU两个并联的投影
  • 并联投影按照顺序依次尝试
  • 直到有一个投影成功了就返回结果

graph LR subgraph source source-UserId[UserId] source-UId[UId] source-UUserName[UUserName] source-UserUName[UserUName] end subgraph expected expected-Id[Id] expected-UserName[UserName] expected-UName[UName] end source-UserId -->|RemoveUser| expected-Id source-UId -->|RemoveU| expected-Id source-UUserName -->|RemoveU| expected-UserName source-UserUName -->|RemoveUser| expected-UName

二、投影在PocoEmit.Mapper中的应用

1. AddPrefix

1.1 AddPrefix的Case

IMapper mapper = Mapper.Create();
mapper.ConfigureMap<AutoUserDTO, User>()
    .Source
    .AddPrefix("User");
var source = new AutoUserDTO { UserId = "222", UserName = "Jxj2" };
var converter = mapper.GetConverter<AutoUserDTO, User>();
var result = converter.Convert(source);
Assert.NotNull(result);
Assert.Equal(source.UserId, result.Id.ToString());
Assert.Equal(source.UserName, result.UserName);

1.2 AddPrefix使用的是去除前缀投影(RemovePrefix)

  • AddPrefix("User")会调用Projection.RemovePrefix("User")来创建一个去除前缀投影
  • AutoUserDTO两个成员分别是UserId和UserName
  • User两个成员分别是Id和UserName
  • 对AutoUserDTO投影的效果相当于sql: SELECT UserId,UserName,UserId AS Id FROM AutoUserDTO
  • 通过这个投影,两边的成员名就能完美匹配上了

1.3 影像演示一下这个过程

graph LR subgraph AutoUserDTO source-UserId[UserId] source-UserName[UserName] end subgraph AutoUserDTO投影 projection-UserId[UserId] projection-UserName[UserName] projection-Id[Id] projection-Name[Name] end subgraph User result-Id[Id] result-UserName[UserName] end source-UserId -->|RemoveUser| projection-Id source-UserName -->|RemoveUser| projection-Name source-UserId -->| | projection-UserId source-UserName -->| | projection-UserName result-Id -->| Mapping | projection-Id result-UserName -->| Mapping | projection-UserName

1.4 使用前缀投影(Prefix)也可以实现类似的效果

  • 通过Projection.Prefix("User")创建一个前缀投影
  • 这时就需要对User进行投影
  • 对User投影的效果相当于sql: SELECT Id,UserName,Id AS UserId FROM User
  • PocoEmit.Mapper使用RemovePrefix是为了投影的成员名更短,更容易匹配上
1.4.1 影像演示一下这个过程

graph LR subgraph AutoUserDTO source-UserId[UserId] source-UserName[UserName] end subgraph User result-Id[Id] result-UserName[UserName] end subgraph User投影 projection-Id[Id] projection-UserName[UserName] projection-UserId[UserId] end result-Id -->| | projection-Id result-UserName -->| | projection-UserName result-Id -->|AddUser| projection-UserId projection-UserId -->|Mapping| source-UserId projection-UserName -->|Mapping| source-UserName

2. AddPrefix替换前缀重载

2.1 AddPrefix替换前缀重载的Case

  • AddPrefix("User", "U")会调用Projection.ReplacePrefix("User", "U")
  • 创建了一个替换前缀投影
IMapper mapper = Mapper.Create();
mapper.ConfigureMap<AutoUserDTO, UserCustomDTO>()
    .Source
    .AddPrefix("User", "U");
var source = new AutoUserDTO { UserId = "555", UserName = "Jxj5" };
var converter = mapper.GetConverter<AutoUserDTO, UserCustomDTO>();
var result = converter.Convert(source);
Assert.NotNull(result);
Assert.Equal(source.UserId, result.UId.ToString());
Assert.Equal(source.UserName, result.UName);

2.2 影像演示一下这个过程

graph LR subgraph AutoUserDTO source-UserId[UserId] source-UserName[UserName] end subgraph AutoUserDTO投影 projection-UserId[UserId] projection-UserName[UserName] projection-UId[UId] projection-UName[UName] end subgraph UserCustomDTO result-UId[UId] result-UName[UName] end source-UserId -->| | projection-UserId source-UserName -->| | projection-UserName source-UserId -->|UserToU| projection-UId source-UserName -->|UserToU| projection-UName result-UId --> |Mapping| projection-UId result-UName --> |Mapping| projection-UName

2.3 对UserCustomDTO替换的Case

  • 对UserCustomDTO投影的效果相当于sql: SELECT UId,UName,UId AS UserId,UName AS UserName FROM UserCustomDTO
  • 通过这个投影,两边的成员名也能完美匹配上了
IMapper mapper = Mapper.Create();
mapper.ConfigureMap<AutoUserDTO, UserCustomDTO>()
    .Dest
    .AddPrefix("U", "User");
var source = new AutoUserDTO { UserId = "555", UserName = "Jxj5" };
var converter = mapper.GetConverter<AutoUserDTO, UserCustomDTO>();
var result = converter.Convert(source);
Assert.NotNull(result);
Assert.Equal(source.UserId, result.UId.ToString());
Assert.Equal(source.UserName, result.UName);

2.4 UserCustomDTO替换的影像

graph LR subgraph AutoUserDTO source-UserId[UserId] source-UserName[UserName] end subgraph UserCustomDTO result-UId[UId] result-UName[UName] end subgraph UserCustomDTO投影 projection-UserId[UserId] projection-UserName[UserName] projection-UId[UId] projection-UName[UName] end result-UId -->| | projection-UId result-UName -->| | projection-UName result-UId -->| UToUser | projection-UserId result-UName -->| UToUser | projection-UserName projection-UserId -->|Mapping| source-UserId projection-UserName -->|Mapping| source-UserName

3. AddSuffix

3.1 AddSuffix替换后缀的Case

  • AddSuffix("y", "ies")会调用Projection.ReplaceSuffix("y", "ies")
  • 创建一个替换后缀投影
  • 常用于单复数拼写转化
  • 对Customer投影的效果相当于sql: SELECT Name,City,Name AS CustomerName,City AS CustomerCities FROM Customer
  • ConfigureMap方法默认会把源类名作为目标的前缀,也把目标类名作为源的前缀,
  • 所以在这个例子里没有显示调用AddPrefix,Customer前缀自动生效了
  • 如果不需要默认规则可以通过参数autoRecognize设置为false禁用掉
  • AddSuffix也支持去掉后缀的重载与AddPrefix类似,用法简单,这里就不展示了
IMapper mapper = Mapper.Create();
mapper.ConfigureMap<Customer, CustomerDTO>()
    .Source
    .AddSuffix("y", "ies");
var source = new Customer("Jxj", "北京");
var converter = mapper.GetConverter<Customer, CustomerDTO>();
var result = converter.Convert(source);
Assert.NotNull(result);
Assert.Equal(source.Name, result.CustomerName);
Assert.Equal(source.City, result.CustomerCities);

3.2 AddSuffix替换后缀的影像

  • 上面文字描述的内容比较抽象
  • 大家看以下影像就清楚了
  • 借助强大的投影能力
  • Mapping就很简单了,直接同名映射(一般忽略大小写,是否忽略大小写可以配置)

graph LR subgraph Customer source-Name[Name] source-City[City] end subgraph Customer投影 projection-Name[Name] projection-City[City] projection-Cities[Cities] end subgraph CustomerDTO投影 projection2-CustomerName[CustomerName] projection2-CustomerCities[CustomerCities] projection2-Name[Name] projection2-Cities[Cities] end subgraph CustomerDTO result-CustomerName[CustomerName] result-CustomerCities[CustomerCities] end source-Name -->| | projection-Name source-City -->| | projection-City source-City -->|yToies| projection-Cities result-CustomerName -->| | projection2-CustomerName result-CustomerCities -->| | projection2-CustomerCities result-CustomerName -->| RemoveCustomer | projection2-Name result-CustomerCities -->|RemoveCustomer| projection2-Cities projection2-Name -->|Mapping| projection-Name projection2-Cities -->|Mapping| projection-Cities

4. AddProjection

4.1 AddProjection的Case

  • Projection.Replace("Nume", "Name")创建替换"Nume"为"Name"的投影
  • 通过AddProjection引用投影,来实现更个性化的映射规则
  • 实现了可扩展的映射规则,不仅限于前缀或后缀的规则
IMapper mapper = Mapper.Create();
mapper.ConfigureMap<ProductJson, Product>()
    .Source
    .AddProjection(Projection.Replace("Nume", "Name"));
var source = new ProductJson { CityNume = "北京", ProductNumeCount = 999 };
var converter = mapper.GetConverter<ProductJson, Product>();
var result = converter.Convert(source);
Assert.NotNull(result);
Assert.Equal(source.CityNume, result.CityName);
Assert.Equal(source.ProductNumeCount, result.ProductNameCount);

4.2 AddProjection的影像

graph LR subgraph ProductJson source-CityNume[CityNume] source-ProductNumeCount[ProductNumeCount] end subgraph ProductJson投影 projection-CityNume[CityNume] projection-ProductNumeCount[ProductNumeCount] projection-CityName[CityName] projection-ProductNameCount[ProductNameCount] end subgraph Product result-CityName[CityName] result-ProductNameCount[ProductNameCount] end source-CityNume -->| | projection-CityNume source-ProductNumeCount -->| | projection-ProductNumeCount source-CityNume -->|NumeToName| projection-CityName source-ProductNumeCount -->|NumeToName| projection-ProductNameCount result-CityName -->|Mapping| projection-CityName result-ProductNameCount -->|Mapping| projection-ProductNameCount

三、总结

  • 借助投影技术极大的增强了PocoEmit.Mapper的映射能力
  • 另外还增加了PocoEmit.Mapper映射的可扩展性
  • 用户可以根据需要创建自己的投影来实现个性化的映射规则
  • Mapper只是投影的一个应用场景,笔者还在开发投影在SourceGenerator的应用
  • 投影项目hand.projections就是把类似代码抽象成一个独立的库
  • 每个投影功能很简单,不同场景下复用并组合来实现复杂需求才是投影的魅力所在
  • 更多PocoEmit.Mapper可以查看本合集其他文章
  • dotnet add package PocoEmit.Mapper --version 0.8.8.1-alpha

投影库的源码地址:
github: https://github.com/donetsoftwork/HandCore.net/tree/master/Hand.Projections
gitee同步更新:https://gitee.com/donetsoftwork/HandCore.net/tree/master/Hand.Projections

PocoEmit.Mapper源码地址
github: https://github.com/donetsoftwork/MyEmit/tree/main/PocoEmit.Mapper
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit/tree/main/PocoEmit.Mapper

感兴趣的同学可以去看看源码,欢迎star和pr