Achieve Prisma-like Developer Experience in EF Core! Introduction to Linqraft

Published: (December 1, 2025 at 07:49 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Motivation

C# is a wonderful language. With powerful type safety, a rich standard library, and the ability to handle everything from GUI app development to web development, I think it’s an excellent language for almost any purpose.

However, there’s something about C# that has been frustrating me on a daily basis.

Defining classes is tedious! and Writing Select queries is tedious!

Since C# is a statically‑typed language, you basically have to define all the classes you want to use. While this is unavoidable to some extent, having to define derived classes every time is extremely tedious.

Especially when using an ORM (Object‑Relational Mapping) for database access, the shape of data you want must be defined as a DTO (Data Transfer Object) every time, resulting in writing similar class definitions over and over again.

Prisma vs. EF Core

In Prisma (a TypeScript ORM) you can write:

// 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 }[];
// }[];

If you try to do the same thing in C#‘s EF Core, it looks like this:

// 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; }
}

Even though we already have a User class, it seems like it could be auto‑generated from there…

When the query becomes more complex, the amount of boilerplate grows dramatically:

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: All DTO class definitions used above also need to be defined.

Null‑checking pain points

EF Core’s Select expressions cannot use the null‑conditional operator (?.) inside Expression. Consequently, you must write verbose ternary checks:

// Unbelievably hard to read
Property = o.A != null && o.A.B != null && o.A.B.C != null
    ? o.A.B.C.D
    : null;

The same issue appears when handling collections that may be null:

// Give me a break
Items = o.Child != null
    ? o.Child.Items.Select(i => new ItemDto { /* ... */ }).ToList()
    : new List();

What do you think? I really hate this.

What I Wanted

Looking at the Prisma example again, it has roughly the following features (using TypeScript language features as well):

  • When you write a query once, the corresponding type is generated.
  • You can write ?. directly in queries without worrying about null checking.

After some thought, I realized that by combining anonymous types, source generators, and interceptors, these features could be achieved.

Attempting the Implementation

Using Anonymous Types

C#‘s anonymous types let the compiler generate a class automatically when you write new { ... }:

// No explicit type name after new
var anon = new
{
    Id = 1,
    Name = "Alice",
    IsActive = true
};

They are handy for defining disposable classes inside Select queries:

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);
}

The limitation is that anonymous types have no name, so they cannot be used as method arguments or return values. This restriction makes them less useful in many scenarios.

Auto‑generating Corresponding Classes

If we create a source generator that automatically generates concrete classes based on the shapes of anonymous types used in queries, we could overcome the above limitation. This is exactly what Linqraft does.

(The article continues with implementation details…)

Back to Blog

Related posts

Read more »

Convert Excel to PDF in C# Applications

Overview Transforming Excel files into polished, share‑ready PDFs doesn’t have to be a slow or complicated process. With the GroupDocs.Conversion Cloud SDK for...