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.
{ 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.
Getting Started
Projections are part of the HotChocolate.Data package.
dotnet add package HotChocolate.DataHotChocolate.* packages need to have the same version.Register projections on the schema:
// Program.csbuilder .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;}
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.
// 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:
// 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;
Combining with Filtering, Sorting, and Pagination
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.cspublic 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 } } }}
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:
// 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}
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:
// Models/User.cspublic 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:
// Models/User.cspublic 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
- Need to filter results? See Filtering.
- Need to sort results? See Sorting.
- Need to page through results? See Pagination.
- Need to integrate with Entity Framework? See Entity Framework Integration.