在 EF Core 中实现类似 Prisma 的开发者体验!Linqraft 介绍
Source: Dev.to
动机
C# 是一门很棒的语言。它拥有强大的类型安全、丰富的标准库,并且能够处理从 GUI 应用开发到 Web 开发的所有场景,我认为它是几乎所有用途的优秀语言。
然而,C# 有一点让我每天都感到沮丧。
定义类很繁琐! 并且 编写 Select 查询很繁琐!
由于 C# 是静态类型语言,你基本上必须定义所有想要使用的类。虽然在一定程度上这是不可避免的,但每次都要定义派生类实在是太麻烦了。
尤其是在使用 ORM(对象关系映射)进行数据库访问时,你每次都必须把想要的数据显示为 DTO(数据传输对象),于是要一遍又一遍地编写相似的类定义。
Prisma vs. EF Core
在 Prisma(一个 TypeScript ORM)中,你可以这样写:
// user type is automatically generated from schema file
const users = await prisma.user.findMany({
// Specify the data you want with select
select: {
id: true,
name: true,
posts: {
// You can also specify related table data with select
select: {
title: true,
},
},
},
});
// The type of users automatically becomes:
// type Users = {
// id: number;
// name: string;
// posts: { title: string }[];
// }[];
如果在 C# 的 EF Core 中尝试同样的操作,则会是这样:
// Assume Users type is defined in a separate file
var users = dbContext.Users
// Specifying the data you want with Select is the same
.Select(u => new UserWithPostDto
{
Id = u.Id,
Name = u.Name,
// Child classes are also specified with Select in the same way
Posts = u.Posts.Select(p => new PostDto { Title = p.Title }).ToList()
})
.ToList();
// You have to define the DTO class yourself!
public class UserWithPostDto
{
public int Id { get; set; }
public string Name { get; set; }
public List Posts { get; set; }
}
// Same for child classes
public class PostDto
{
public string Title { get; set; }
}
即使我们已经有了 User 类,它似乎也可以从中自动生成……
当查询变得更复杂时,样板代码的数量会急剧增长:
var result = dbContext.Orders
.Select(o => new OrderDto
{
Id = o.Id,
Customer = new CustomerDto
{
CustomerId = o.Customer.Id,
CustomerName = o.Customer.Name,
// Tedious part
CustomerAddress = o.Customer.Address != null
? o.Customer.Address.Location
: null,
// Wrap in another DTO because we don't want to check every time
AdditionalInfo = o.Customer.AdditionalInfo != null
? new CustomerAdditionalInfoDto
{
InfoDetail = o.Customer.AdditionalInfo.InfoDetail,
CreatedAt = o.Customer.AdditionalInfo.CreatedAt
}
: null
},
Items = o.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
Quantity = i.Quantity,
// Same for arrays. Hard to read...
ProductComments = i.CommentInfo != null
? i.CommentInfo.Comments.Select(c => new ProductCommentDto
{
CommentText = c.CommentText,
CreatedBy = c.CreatedBy
}).ToList()
: new List()
}).ToList()
})
.ToList();
Note: 上面使用的所有 DTO 类定义也都需要手动编写。
空值检查的痛点
EF Core 的 Select 表达式 不能在 Expression 中使用空值条件运算符 (?.)。因此,你必须写冗长的三元运算检查:
// Unbelievably hard to read
Property = o.A != null && o.A.B != null && o.A.B.C != null
? o.A.B.C.D
: null;
处理可能为 null 的集合时也会出现同样的问题:
// Give me a break
Items = o.Child != null
? o.Child.Items.Select(i => new ItemDto { /* ... */ }).ToList()
: new List();
你怎么看?我真的受够了。
我的需求
再次看 Prisma 示例,它大致具备以下特性(同时利用了 TypeScript 语言特性):
- 当你写一次查询时,会自动生成对应的类型。
- 你可以在查询中直接使用
?.,而不必担心空值检查。
经过思考,我意识到通过结合 匿名类型、源生成器 和 拦截器,这些特性是可以实现的。
实现尝试
使用匿名类型
C# 的匿名类型会在你写 new { ... } 时让编译器自动生成一个类:
// No explicit type name after new
var anon = new
{
Id = 1,
Name = "Alice",
IsActive = true
};
它们在 Select 查询内部定义一次性类时非常方便:
var users = dbContext.Users
.Select(u => new
{
Id = u.Id,
Name = u.Name,
Posts = u.Posts.Select(p => new { Title = p.Title }).ToList()
})
.ToList();
// You can access and use it normally
var user = users[0];
Console.WriteLine(user.Name);
foreach (var post in user.Posts)
{
Console.WriteLine(post.Title);
}
限制在于匿名类型没有名字,不能作为方法参数或返回值使用。这个限制让它在很多场景下的实用性受限。
自动生成对应的类
如果我们创建一个 源生成器,能够根据查询中使用的匿名类型形状自动生成具体类,就可以突破上述限制。这正是 Linqraft 所做的事情。
(文章随后继续实现细节…)