Table of Contents

BytLabs.Api.Graphql

HotChocolate GraphQL server setup with BytLabs conventions baked in: observability, mutation conventions, authorization, runtime type mappings, typed error results, DTO/command/aggregate type registration helpers, and dynamic-data filter input types.

Install

<PackageReference Include="BytLabs.Api.Graphql" />
<PackageReference Include="HotChocolate.Data.MongoDb" />

What's inside

Type / method Purpose
AddGraphQLService() AddGraphQLServer() + BytLabs defaults (observability, mutation conventions, runtime type mappings, authorization)
AddDynamicDataTypes() Registers the dynamic-data filter input type (DataOperationFilterInputType)
AddDefaultQuerySettings(name) Paging (max 50) + projections/filtering/sorting + cursor paging
AddCommandType<TCommand>() Registers a command as a GraphQL input type (...Command/...Dto...Input)
AddDtoType<T>() Registers a DTO as an object type via DtoType<T> (strips Dto from the name)
AddAggregateFilterType<TAgg,TId>() / AddAggregateSortType<TAgg,TId>() Dynamic-data-aware filter/sort input types for an aggregate
DtoType<T> Base object type for DTOs (extend it to add computed/resolved fields)
ErrorTypes.Business.BusinessError, ErrorTypes.Validation.ValidationError / FieldError Typed mutation error results
Observability.GlobalErrorFilter, ErrorLoggingDiagnosticsEventListener Error logging/translation

Registration

services.AddGraphQLService()
    .AddMongoDbQuerySettings()      // from your Api.Utils (Mongo filtering/projection/sorting/paging)
    .AddDynamicDataTypes()          // dynamic-data 'where' input
    .AddCommandTypes()              // your AddCommandType<...>() group
    .AddDtoTypes()                  // your AddDtoType<...>()/AddType<CustomDtoType>() group
    .AddAggregateTypes()            // your AddAggregateFilterType/AddAggregateSortType group
    .AddMutationType<Mutation>()
    .AddQueryType<Query>()
    .ModifyPagingOptions(o => o.IncludeTotalCount = true);

AddGraphQLService() already enables mutation conventions ({Name}Input/{Name}Payload/ {Name}Error, input argument, errors field), authorization, observability, and runtime type mappings (GuidIdType). You do not re-add mutation conventions per service.

Usage

Query (partial Query class)

public partial class Query
{
    [UsePaging, UseProjection, UseFiltering(Type = typeof(Order)), UseSorting(Type = typeof(Order))]
    public IExecutable<OrderDto> GetOrders([Service] IMongoDatabase db)
        => db.GetCollection<Order>().Aggregate()
             .Project(Builders<Order>.Projection.As<OrderDto>())
             .AsExecutable();
}

Mutation with typed errors (partial Mutation class)

public partial class Mutation
{
    [Authorize]
    [Error(typeof(BusinessError))]
    [Error(typeof(ValidationError))]
    public async Task<ProductDto> CreateProduct(CreateProductCommand input, [Service] IMediator mediator, CancellationToken ct)
        => await mediator.Send(input, ct);
}

DomainExceptionBusinessError; FluentValidation/CommandValidationExceptionValidationError (with FieldError[] fields). These appear in the mutation payload's errors union.

Registering types

public static IRequestExecutorBuilder AddCommandTypes(this IRequestExecutorBuilder b) => b
    .AddCommandType<CreateProductCommand>()      // → CreateProductInput
    .AddCommandType<RemoveProductCommand>();

public static IRequestExecutorBuilder AddDtoTypes(this IRequestExecutorBuilder b) => b
    .AddDtoType<OrderDto>()                       // → object type "Order"
    .AddType<ProductDtoType>();                   // custom DtoType with computed fields

public static IRequestExecutorBuilder AddAggregateTypes(this IRequestExecutorBuilder b) => b
    .AddAggregateFilterType<Product, Guid>()     // → ProductFilterInput (dynamic-data aware)
    .AddAggregateSortType<Product, Guid>();      // → ProductSortInput

Custom DTO object type (computed fields)

Extend DtoType<T> to add resolved fields:

public class ProductDtoType : DtoType<ProductDto>
{
    protected override void Configure(IObjectTypeDescriptor<ProductDto> descriptor)
    {
        base.Configure(descriptor);
        descriptor.Field("variantCount")
            .Type<NonNullType<IntType>>()
            .Resolve(ctx => ctx.Parent<ProductDto>().Variants?.Count ?? 0);
    }
}

Dynamic-data filtering

AddDynamicDataTypes() + AddAggregateFilterType<TAgg,TId>() expose a where argument that filters over the aggregate's schema-less Data. Read it in a resolver via context.ArgumentValue<InputFilteringDynamicData?>("where") and pass it to the MongoDB ApplyDynamicDataFilteration(...) helper (see BytLabs.DataAccess.MongoDB).