Skip to main content

Entity Configuration

Field-level renames, ignores, and directives for the generated grouping types.

Each [UseGrouping] field produces three types per entity (*Grouping, *GroupingKey, *Aggregate). Rather than requiring a separate config class per generated type, the library:

  • inherits field renames and ignores from the entity's ObjectType<T> by default, so one declaration covers all three;
  • offers GroupingConfig<T> as a single override surface for grouping-specific behaviour.

Inheriting from ObjectType<T>

public class WidgetType : ObjectType<Widget>
{
    protected override void Configure(IObjectTypeDescriptor<Widget> descriptor)
    {
        descriptor.Field(w => w.Label).Name("displayName");   // displayName in key + aggregate
        descriptor.Ignore(w => w.InternalNotes);              // hidden from all grouping types
    }
}

Renames and ignores propagate to every appearance of the field in the grouping types, including nested aggregates.

Overriding with GroupingConfig<T>

For grouping-specific behaviour the entity's ObjectType<T> can't express:

public class EmployeeGroupingConfig : GroupingConfig<Employee>
{
    protected override void Configure(IGroupingConfigDescriptor<Employee> descriptor)
    {
        descriptor.Field(e => e.InternalNotes).Ignore();                              // hide from grouping only
        descriptor.Field(e => e.Bonus).Name("performanceBonus");                      // rename in grouping only
        descriptor.Field(e => e.Salary).Directive(new AuthorizeDirective("payroll")); // attach a directive
    }
}

Register one call per config class:

builder.Services
    .AddGraphQL()
    .AddGrouping()
    .AddGroupingConfig<EmployeeGroupingConfig>()
    .AddQueryType<Query>();

When both an ObjectType<T> and a GroupingConfig<T> exist, the GroupingConfig<T> wins for grouping types; the ObjectType<T> rename still applies to the entity.

Bulk registration via source generator

For many GroupingConfig<T> subclasses, install the companion analyzer:

<PackageReference Include="Community.HotChocolate.Data.Grouping.Analyzers" Version="x.y.z" />

It emits an AddGroupingConfigs() extension that registers every config in the consumer's assembly:

builder.Services
    .AddGraphQL()
    .AddGrouping()
    .AddGroupingConfigs()
    .AddQueryType<Query>();

The generator only scans the consumer's own assembly; configs in referenced libraries need explicit registration. With [assembly: HotChocolate.Module("MyApp")] the generated method is renamed to AddMyAppGroupingConfigs().

The generator picks up non-abstract, non-generic subclasses (public or internal) with a public constructor, matching how HotChocolate activates types via AddType<T>().

Injecting services into a config

GroupingConfig<T> is activated through ActivatorUtilities, so it can take DI constructor parameters. Annotate the chosen ctor with [ActivatorUtilitiesConstructor] if there are multiple:

public class EmployeeGroupingConfig : GroupingConfig<Employee>
{
    private readonly IFeatureGate _gate;
 
    [ActivatorUtilitiesConstructor]
    public EmployeeGroupingConfig(IFeatureGate gate) => _gate = gate;
 
    protected override void Configure(IGroupingConfigDescriptor<Employee> descriptor)
    {
        if (!_gate.IsEnabled(Feature.ExposeSalary))
        {
            descriptor.Field(e => e.Salary).Ignore();
        }
    }
}

The [UseGrouping] Attribute

[UseGrouping]
public IQueryable<Employee> GetEmployeeGrouping(MyDbContext db) => db.Employees;

Renames and ignores in the grouping schema are configured exclusively through GroupingConfig<T> (above).

Scope

When two grouping conventions need to coexist:

[UseGrouping(Scope = "reporting")]
public IQueryable<Employee> GetEmployeeGrouping(MyDbContext db) => db.Employees;
 
// or fluently:
descriptor.Field("employees").UseGrouping(scope: "reporting");

The scope gates which GroupingConvention (registered via AddGrouping(scope: ...)) is consulted.