C
C#11mo ago
Haeri

EF Core Nested Projections

I am trying to find out a good way to perform a projection on a model with multiple relationship levels. Here is an example:
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public int Other { get; set; }
public User User { get; set; }
}

public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public int Other { get; set; }
}

public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }
}

public class UserDTO
{
public int Id { get; set; }
public string UserName { get; set; }
}

var result = dbContext.Blogs
.Select(blog => new BlogDTO
{
Id = blog.Id,
Title = blog.Title,
User = new UserDTO
{
Id = blog.User.Id,
UserName = blog.User.UserName
}
})
.toList();
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public int Other { get; set; }
public User User { get; set; }
}

public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public int Other { get; set; }
}

public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }
}

public class UserDTO
{
public int Id { get; set; }
public string UserName { get; set; }
}

var result = dbContext.Blogs
.Select(blog => new BlogDTO
{
Id = blog.Id,
Title = blog.Title,
User = new UserDTO
{
Id = blog.User.Id,
UserName = blog.User.UserName
}
})
.toList();
This works but I have a lot more levels in my code and writing this out every time is super tedious. I'l looking for a way to reuse the projections so I don't have to write them out every time.
7 Replies
Haeri
HaeriOP11mo ago
I found two suggestions: Explicit Operator
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }

public static explicit operator BlogDTO(Blog blog) => new()
{
Id = blog.Id,
Title = blog.Title,
User = (UserDTO)blog.User
};
}

/* same for User -> UserDTO */

var result = dbContext.Blogs
.Select(e => (BlogDTO)e)
.toList();
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }

public static explicit operator BlogDTO(Blog blog) => new()
{
Id = blog.Id,
Title = blog.Title,
User = (UserDTO)blog.User
};
}

/* same for User -> UserDTO */

var result = dbContext.Blogs
.Select(e => (BlogDTO)e)
.toList();
This makes life a lot easier but unfortunately I found out that the SQL generated by this does not use projection but always gets the all properties. Expression Functions
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }

public static Expression<Func<Blog, BlogDTO>> BlogToDTO()
{
return blog=> new BlogDTO
{
Id = blog.Id,
Title = blog.Title,
User = UserToDto().Compile()(blog.User)
};
}
}

/* same for User -> UserDTO */

var result = dbContext.Blogs
.Select(BlogDTO.BlogToDTO())
.toList();
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }

public static Expression<Func<Blog, BlogDTO>> BlogToDTO()
{
return blog=> new BlogDTO
{
Id = blog.Id,
Title = blog.Title,
User = UserToDto().Compile()(blog.User)
};
}
}

/* same for User -> UserDTO */

var result = dbContext.Blogs
.Select(BlogDTO.BlogToDTO())
.toList();
But this gives me the following error:
The client projection contains a reference to a constant expression of 'System.Func<User, UserDTO>'. This could potentially cause a memory leak; consider assigning this constant to a local variable and using the variable in the query instead.
The client projection contains a reference to a constant expression of 'System.Func<User, UserDTO>'. This could potentially cause a memory leak; consider assigning this constant to a local variable and using the variable in the query instead.
Mayor McCheese
Mayor McCheese11mo ago
Can you elaborate on this
This works but I have a lot more levels in my code and writing this out every time is super tedious
how many places are you having to write the same code?
Joschi
Joschi11mo ago
Mapperly can generate reusable projections for you, maybe take a look what their Source Gen outputs?
Haeri
HaeriOP11mo ago
For example I have a lot more Models that have a relationship with User so I need to write the same code multiple times for every relationship @Joschi As far as I can see the mapper just auto generates mappings https://mapperly.riok.app/docs/getting-started/generated-mapper-example/#the-generated-code But I don't think I can directly use it in the projection. The next best thing I found was this: https://mapperly.riok.app/docs/configuration/queryable-projections/ But I will run into the same issue there because I would have to do something like this:
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }
}

public static class BlogMapper
{
public static IQueryable<BlogDTO> ProjectToDto(this IQueryable<Blog> query)
{
return query.Select(authUser => new BlogDTO()
{
Id = blog.Id,
Title = blog.Title,
User = new UserDTO() // I have to again manually map User
{
Id = blog.User.Id
UserName = blog.User.UserName
}
});
}
}
public class BlogDTO
{
public int Id { get; set; }
public string Title { get; set; }
public UserDTO User { get; set; }
}

public static class BlogMapper
{
public static IQueryable<BlogDTO> ProjectToDto(this IQueryable<Blog> query)
{
return query.Select(authUser => new BlogDTO()
{
Id = blog.Id,
Title = blog.Title,
User = new UserDTO() // I have to again manually map User
{
Id = blog.User.Id
UserName = blog.User.UserName
}
});
}
}
Mayor McCheese
Mayor McCheese11mo ago
I mean how many entities do you actually have? I feel like you're overthinking to avoid writing some basic mapping code.
Haeri
HaeriOP11mo ago
Here is one of them: Yes I can copy paste but I feel like it is error prone. Especially when I have to add/remove properties and the forget to update it somewhere
No description
Joschi
Joschi11mo ago
I don't see the problem. Unless you want different sets of properties each time, but then I guess you will have to do it by hand.
Want results from more Discord servers?
Add your server