Every GraphQL request specifies exactly what data should be returned. Over or under fetching can be reduced or even eliminated. Hot Chocolate projections leverage this concept and directly projects incoming queries to the database.
Projections operate on IQueryable
by default, but it is possible to create custom providers for projections
to support a specific database driver.
⚠️ Note: Projections currently need a public setter on fields they operate on in order to function correctly. Otherwise the default constructed value will be returned upon query.
{ 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"
Getting Started
Filtering is part of the HotChocolate.Data
package.
dotnet add package HotChocolate.Data
HotChocolate.*
packages need to have the same version.To use projections with your GraphQL endpoint you have to register projections on the schema:
services.AddGraphQLServer() // Your schema configuration .AddProjections();
Projections can be registered on a field. A middleware will apply the selected fields on the result.
Support for IQueryable
comes out of the box.
The projection middleware will create a projection for the whole subtree of its field. Only fields that
are members of a type will be projected. Fields that define a custom resolver cannot be projected
to the database. If the middleware encounters a field that specifies UseProjection()
this field will be skipped.
public class Query{ [UseProjection] public IQueryable<User> GetUsers([Service] IUserRepository repository) => repository.GetUsers();}
⚠️ Note: If you use more than one middleware, keep in mind that ORDER MATTERS. The correct order is UsePaging > UseProjection > UseFiltering > UseSorting
FirstOrDefault / SingleOrDefault
If you want to limit the response to a single result, you would have to declare a resolver.
Without returning an IQueryable<>
you lose the ability to use filtering.
There are two extensions you can use to leverage collection.FirstOrDefault()
and collection.SingleOrDefault()
to
the GraphQL layer. The extensions will rewrite the response type to the element type of the collection apply the behavior.
public class Query { [UseFirstOrDefault] [UseProjection] [UseFiltering] public IQueryable<User> GetUsers([ScopedService] SomeDbContext someDbContext) { return someDbContext.Users; } }
type Query { users(where: UserFilterInput): User}
type User { id: Int! name: String! email: String!}
Sorting Filtering and Paging
Projections can be used together with sorting, filtering and paging. The order of the middlewares must be correct. Make sure to have the following order: UsePaging > UseProjection > UseFiltering > UseSorting
Filtering and sorting can be projected over relations. Projections cannot project paging over relations.
public class Query{ [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public IQueryable<User> GetUsers([ScopedService] SomeDbContext someDbContext) { return someDbContext.Users; }}
public class User{ public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
[UseFiltering] [UseSorting] public ICollection<Address> Addresses { get; set; }}
{ users( where: { name: { eq: "ChilliCream" } } order: [{ name: DESC }, { email: DESC }] ) { nodes { email addresses(where: { street: { eq: "Sesame Street" } }) { street } } pageInfo { endCursor hasNextPage hasPreviousPage startCursor } }}
SELECT "t"."Email", "t"."Id", "a"."Street", "a"."Id"FROM ( SELECT "u"."Email", "u"."Id", "u"."Name" FROM "Users" AS "u" WHERE "u"."Name" = @__p_0 ORDER BY "u"."Name" DESC, "u"."Email" DESC LIMIT @__p_1) AS "t"LEFT JOIN "Address" AS "a" ON "t"."Id" = "a"."UserId"ORDER BY "t"."Name" DESC, "t"."Email" DESC, "t"."Id", "a"."Id"
Always Project Fields
Resolvers on types often access data of the parent, e.g. uses the Email
member of the parent to fetch some
related data from another service. With projections, this resolver could only work when the user also queries
for the email
field. To ensure a field is always projected you have to use IsProjected(true)
.
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; }}
{ users { 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"
Exclude fields
If a projected field is requested, the whole subtree is processed. Sometimes you want to opt out of projections.
The projections middleware skips a field in two cases. Either the visitor encounters a field that is a UseProjection
field
itself, or it defines IsProjected(false)
.
public class User{ public int Id { get; set; } public string Name { get; set; } [IsProjected(false)] public string Email { get; set; } public Address Address { get; set; }}
{ users { email address { street } }}
SELECT "a"."Id" IS NOT NULL, "a"."Street"FROM "Users" AS "u"LEFT JOIN "Address" AS "a" ON "u"."AddressId" = "a"."Id"