website/src/docs/hotchocolate/v16/resolvers-and-data/projections.md
GraphQL clients specify which fields they need. Projections take advantage of this by translating the requested fields directly into optimized database queries. If a client requests only name and email, Hot Chocolate queries only those columns from the database.
{
users {
email
address {
street
}
}
}
SELECT "u"."Email", "a"."Id" IS NOT NULL, "a"."Street"
FROM "Users" AS "u"
LEFT JOIN "Address" AS "a" ON "u"."AddressId" = "a"."Id"
Projections operate on IQueryable by default. Custom providers can extend this to other data sources.
Projections require a public setter on fields they operate on. Without a public setter, the default-constructed value is returned.
Projections are part of the HotChocolate.Data package.
Register projections on the schema:
// Program.cs
builder
.AddGraphQL()
.AddProjections();
Apply the [UseProjection] attribute to a resolver that returns IQueryable<T>:
// Types/UserQueries.cs
[QueryType]
public static partial class UserQueries
{
[UseProjection]
public static IQueryable<User> GetUsers(CatalogContext db)
=> db.Users;
}
// Types/UserQueries.cs
public class UserQueries
{
public IQueryable<User> GetUsers(CatalogContext db)
=> db.Users;
}
// Types/UserQueriesType.cs
public class UserQueriesType : ObjectType<UserQueries>
{
protected override void Configure(IObjectTypeDescriptor<UserQueries> descriptor)
{
descriptor.Field(f => f.GetUsers(default!)).UseProjection();
}
}
The projection middleware creates a Select expression for the entire subtree of the field. Fields with custom resolvers are not projected to the database. If the middleware encounters a nested field that also specifies UseProjection(), that field is handled separately.
Middleware order matters. When combining multiple middleware, apply them in this order:
UsePaging>UseProjection>UseFiltering>UseSorting.
In v16, QueryContext<T> provides an alternative to the [UseProjection] middleware. Instead of applying projections as middleware, you return a QueryContext<T> from your resolver and Hot Chocolate applies projections, filtering, and sorting at execution time.
// Types/UserQueries.cs
[QueryType]
public static partial class UserQueries
{
public static QueryContext<User> GetUsers(CatalogContext db)
=> db.Users.AsQueryContext();
}
QueryContext<T> integrates projection, filtering, and sorting into a single return type. This can reduce middleware stacking and make your resolver signatures cleaner.
Do not combine QueryContext<T> with [UseProjection] on the same field. The HC0099 analyzer warns when both are present because they conflict: each tries to apply its own Select expression, leading to unexpected behavior or runtime errors.
Incorrect:
// This triggers HC0099
[UseProjection]
public static QueryContext<User> GetUsers(CatalogContext db)
=> db.Users.AsQueryContext();
Correct: Use one approach or the other:
// Option 1: QueryContext<T> (handles projections internally)
public static QueryContext<User> GetUsers(CatalogContext db)
=> db.Users.AsQueryContext();
// Option 2: [UseProjection] middleware
[UseProjection]
public static IQueryable<User> GetUsers(CatalogContext db)
=> db.Users;
Projections work with filtering, sorting, and pagination. Maintain the correct middleware order:
// Types/UserQueries.cs
[QueryType]
public static partial class UserQueries
{
[UsePaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public static IQueryable<User> GetUsers(CatalogContext db)
=> db.Users;
}
Filtering and sorting can project over relationships. Projections cannot project pagination over relationships. For nested collections that need filtering or sorting, apply those attributes to the collection property:
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[UseFiltering]
[UseSorting]
public ICollection<Address> Addresses { get; set; }
}
{
users(where: { name: { eq: "ChilliCream" } }, order: [{ name: DESC }]) {
nodes {
email
addresses(where: { street: { eq: "Sesame Street" } }) {
street
}
}
}
}
When you want a field to return a single entity instead of a list, use [UseFirstOrDefault] or [UseSingleOrDefault]. These rewrite the return type from IQueryable<T> to T? and apply the corresponding LINQ operation:
// Types/UserQueries.cs
[QueryType]
public static partial class UserQueries
{
[UseFirstOrDefault]
[UseProjection]
[UseFiltering]
public static IQueryable<User> GetUser(CatalogContext db)
=> db.Users;
}
This produces a schema field that returns a single User (or null) instead of a list:
type Query {
user(where: UserFilterInput): User
}
Resolvers on a type sometimes need data from the parent that the client did not request. Mark a field with [IsProjected(true)] to ensure it is always included in the database query:
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[IsProjected(true)]
public string Email { get; set; }
public Address Address { get; set; }
}
// Types/UserType.cs
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(f => f.Email).IsProjected(true);
}
}
Even if the client does not request email, the SQL query includes the Email column so that resolvers depending on it have the data they need.
Use [IsProjected(false)] to exclude a field from projection. The field remains in the schema but is not included in the database query:
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[IsProjected(false)]
public string InternalNotes { get; set; }
public Address Address { get; set; }
}
// Types/UserType.cs
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(f => f.InternalNotes).IsProjected(false);
}
}