Projections

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.

GraphQL
{
users {
email
address {
street
}
}
}
SQL
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.

Getting Started

Projections are part of the HotChocolate.Data package.

Bash
dotnet add package HotChocolate.Data
Warning
All HotChocolate.* packages need to have the same version.

Register projections on the schema:

C#
// Program.cs
builder
.AddGraphQL()
.AddProjections();

Apply the [UseProjection] attribute to a resolver that returns IQueryable<T>:

C#
// Types/UserQueries.cs
[QueryType]
public static partial class UserQueries
{
[UseProjection]
public static IQueryable<User> GetUsers(CatalogContext db)
=> db.Users;
}

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.

QueryContext<T> Pattern

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.

C#
// 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.

HC0099 Analyzer Warning

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:

C#
// This triggers HC0099
[UseProjection]
public static QueryContext<User> GetUsers(CatalogContext db)
=> db.Users.AsQueryContext();

Correct: Use one approach or the other:

C#
// 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;

Combining with Filtering, Sorting, and Pagination

Projections work with filtering, sorting, and pagination. Maintain the correct middleware order:

C#
// 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:

C#
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[UseFiltering]
[UseSorting]
public ICollection<Address> Addresses { get; set; }
}
GraphQL
{
users(where: { name: { eq: "ChilliCream" } }, order: [{ name: DESC }]) {
nodes {
email
addresses(where: { street: { eq: "Sesame Street" } }) {
street
}
}
}
}

FirstOrDefault / SingleOrDefault

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:

C#
// 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:

GraphQL
type Query {
user(where: UserFilterInput): User
}

Always Project Fields

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:

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

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.

Exclude Fields from Projection

Use [IsProjected(false)] to exclude a field from projection. The field remains in the schema but is not included in the database query:

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

Next Steps

Last updated on April 13, 2026 by Michael Staib