在 EF Core 中实现类似 Prisma 的开发者体验!Linqraft 介绍

发布: (2025年12月2日 GMT+8 08:49)
6 min read
原文: Dev.to

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 所做的事情。

(文章随后继续实现细节…)

Back to Blog

相关文章

阅读更多 »