Quick Start
A walkthrough with a simplified Employee model from the test suite. Queries and results mirror the verified GroupingTests snapshots.
1. Define the Entity
public record Employee
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public bool Active { get; set; }
public double Salary { get; set; }
public decimal? Bonus { get; set; }
public int? DepartmentId { get; set; }
public Department? Department { get; set; }
public int? CompanyId { get; set; }
public Company? Company { get; set; }
public ICollection<Project>? Projects { get; set; }
public ICollection<Skill>? Skills { get; set; }
}2. Expose a Query Field
public class Query
{
[UseGrouping]
[UseFiltering]
public IQueryable<Employee> GetEmployeeGrouping(MyDbContext db) => db.Employees;
}Generated schema
type Query {
employeeGrouping(
filterNullParent: Boolean = false
where: EmployeeFilterInput
): [EmployeeGrouping!]!
}
type EmployeeGrouping {
key: EmployeeGroupingKey!
count(having: IntOperationFilterInput): Int!
aggregate: EmployeeAggregate!
}
type EmployeeGroupingKey {
id: Int
name: String
active: Boolean
salary: Float
bonus: Decimal
departmentId: Int
department: DepartmentGroupingKey
companyId: Int
company: CompanyGroupingKey
projects: ProjectGroupingKey
skills: SkillGroupingKey
}
type EmployeeAggregate {
id: IntAggregateResult
name: StringAggregateResult
active: BooleanAggregateResult
salary: FloatAggregateResult
bonus: DecimalAggregateResult
departmentId: IntAggregateResult
department: DepartmentAggregate
companyId: IntAggregateResult
company: CompanyAggregate
projects: ProjectAggregate
skills: SkillAggregate
}
type IntAggregateResult {
avg(having: FloatOperationFilterInput): Float
sum(having: LongOperationFilterInput): Long
min(having: IntOperationFilterInput): Int
max(having: IntOperationFilterInput): Int
}
type FloatAggregateResult {
avg(having: FloatOperationFilterInput): Float
sum(having: FloatOperationFilterInput): Float
min(having: FloatOperationFilterInput): Float
max(having: FloatOperationFilterInput): Float
}
type DecimalAggregateResult {
avg(having: DecimalOperationFilterInput): Decimal
sum(having: DecimalOperationFilterInput): Decimal
min(having: DecimalOperationFilterInput): Decimal
max(having: DecimalOperationFilterInput): Decimal
}
type StringAggregateResult {
min(having: StringOperationFilterInput): String
max(having: StringOperationFilterInput): String
}
type BooleanAggregateResult {
min(having: BooleanOperationFilterInput): Boolean
max(having: BooleanOperationFilterInput): Boolean
}
# `*OperationFilterInput` types (from HotChocolate.Data) and `*FilterInput` from `[UseFiltering]`
# omitted for brevity. HAVING needs `AddFiltering()` registered alongside `AddGrouping()`; if it
# isn't, the `having:` arguments above are silently omitted from the generated schema.3. Write a Grouping Query
The dimensions come from the key selection set. There is no separate groupBy argument.
query {
employeeGrouping {
key {
company { name }
department { name }
}
count
}
}[
{ "key": { "company": { "name": "Acme" }, "department": { "name": "Engineering" } }, "count": 2 },
{ "key": { "company": { "name": "Globex" }, "department": { "name": "Engineering" } }, "count": 2 },
{ "key": { "company": { "name": "Acme" }, "department": { "name": "Sales" } }, "count": 1 },
{ "key": { "company": { "name": "Globex" }, "department": { "name": "Sales" } }, "count": 1 },
{ "key": { "company": { "name": "Acme" }, "department": { "name": null } }, "count": 1 },
{ "key": { "company": { "name": "Globex" }, "department": { "name": null } }, "count": 1 }
]The department.name: null buckets contain employees whose Department row exists with a null Name. To exclude employees whose Department navigation is itself null, pass filterNullParent: true.
4. Add Aggregates
Aggregates are field-first: salary { avg sum min max }, not avg { salary }.
query {
employeeGrouping {
key { company { name } }
count
aggregate {
salary { avg sum min max }
bonus { avg }
department { budget { max } } # nested navigation
}
}
}[
{
"key": { "company": { "name": "Acme" } },
"count": 4,
"aggregate": {
"salary": { "avg": 90000, "sum": 360000, "min": 60000, "max": 120000 },
"bonus": { "avg": 15000 },
"department": { "budget": { "max": 500000 } }
}
},
{
"key": { "company": { "name": "Globex" } },
"count": 4,
"aggregate": {
"salary": { "avg": 86250, "sum": 345000, "min": 75000, "max": 95000 },
"bonus": { "avg": 5166.67 },
"department": { "budget": { "max": 700000 } }
}
}
]5. Filter Buckets with having
query {
employeeGrouping {
key { company { name } }
count(having: { gt: 2 })
aggregate {
salary { sum(having: { gt: 100000 }) avg }
}
}
}See Filtering buckets with having for the operator list.
6. Filter Null Parents
query {
employeeGrouping(filterNullParent: true) {
key { department { name } }
}
}filterNullParent: true drops employees whose Department navigation is null before the GROUP BY. Employees whose Department exists with a null Name still appear in the { "name": null } bucket.