Achieve Prisma-like Developer Experience in EF Core! Introduction to Linqraft
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…)