Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,39 @@ private void WriteBatchResolver(
parameter.Key);
break;

case ResolverParameterKind.Selection:
Writer.WriteIndentedLine(
"var args{0} = contexts[0].Selection;",
i);
break;

case ResolverParameterKind.QueryContext:
var entityType = parameter.TypeParameters[0].ToFullyQualified();
Writer.WriteIndentedLine("var args{0}_selection = contexts[0].Selection;", i);
Writer.WriteIndentedLine("var args{0}_filter = global::{1}.GetFilterContext(contexts[0]);",
i,
WellKnownTypes.FilterContextResolverContextExtensions);
Writer.WriteIndentedLine("var args{0}_sorting = global::{1}.GetSortingContext(contexts[0]);",
i,
WellKnownTypes.SortingContextResolverContextExtensions);
Writer.WriteIndentedLine(
"var args{0} = new global::{1}<{2}>(",
i,
WellKnownTypes.QueryContext,
entityType);
using (Writer.IncreaseIndent())
{
Writer.WriteIndentedLine(
"global::{0}.AsSelector<{1}>(args{2}_selection, contexts[0].IncludeFlags),",
WellKnownTypes.HotChocolateExecutionSelectionExtensions,
entityType,
i);
Writer.WriteIndentedLine("args{0}_filter?.AsPredicate<{1}>(),", i, entityType);
Writer.WriteIndentedLine("args{0}_sorting?.AsSortDefinition<{1}>());", i, entityType);
}

break;

default:
// Fallback for any other kind: extract from first context
Writer.WriteIndentedLine(
Expand Down Expand Up @@ -1653,6 +1686,12 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet
WellKnownTypes.ConnectionFlagsHelper);
break;

case ResolverParameterKind.Selection:
Writer.WriteIndentedLine(
"var args{0} = context.Selection;",
i);
break;

case ResolverParameterKind.Unknown:
Writer.WriteIndentedLine(
"var args{0} = _binding_{1}_{2}.Execute<{3}>(context);",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ public static ResolverParameterKind GetParameterKind(
return ResolverParameterKind.ConnectionFlags;
}

if (parameter.IsSelection())
{
return ResolverParameterKind.Selection;
}

return ResolverParameterKind.Unknown;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,9 @@ public static bool IsPagingArguments(this IParameterSymbol parameter)
=> parameter.Type is INamedTypeSymbol namedTypeSymbol
&& namedTypeSymbol.ToDisplayString().StartsWith(WellKnownTypes.PagingArguments);

public static bool IsSelection(this IParameterSymbol parameter)
=> parameter.Type.ToDisplayString() == WellKnownTypes.ISelection;

public static bool IsGlobalState(
this IParameterSymbol parameter,
[NotNullWhen(true)] out string? key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ ResolverParameterKind.EventMessage or
ResolverParameterKind.FieldNode or
ResolverParameterKind.OutputField or
ResolverParameterKind.ClaimsPrincipal or
ResolverParameterKind.ConnectionFlags;
ResolverParameterKind.ConnectionFlags or
ResolverParameterKind.Selection;

public bool RequiresBinding
=> Kind == ResolverParameterKind.Unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ public enum ResolverParameterKind
Argument,
QueryContext,
PagingArguments,
ConnectionFlags
ConnectionFlags,
Selection
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public static class WellKnownTypes
public const string ImmutableArrayOfMiddlewareContext = "System.Collections.Immutable.ImmutableArray<HotChocolate.Resolvers.IMiddlewareContext>";
public const string IList = "System.Collections.IList";
public const string MiddlewareContext = "HotChocolate.Resolvers.IMiddlewareContext";
public const string ISelection = "HotChocolate.Execution.ISelection";

public static HashSet<string> TypeClass { get; } =
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ public class CatalogContext(DbContextOptions<CatalogContext> options) : DbContex

public DbSet<Brand> Brands => Set<Brand>();

public DbSet<Supplier> Suppliers => Set<Supplier>();

public DbSet<SingleProperty> SingleProperties => Set<SingleProperty>();

protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration(new BrandEntityTypeConfiguration());
builder.ApplyConfiguration(new ProductTypeEntityTypeConfiguration());
builder.ApplyConfiguration(new ProductEntityTypeConfiguration());
builder.ApplyConfiguration(new SupplierEntityTypeConfiguration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ public void Configure(EntityTypeBuilder<Brand> builder)
builder
.Property(cb => cb.Name)
.HasMaxLength(100);

builder
.HasOne(cb => cb.Supplier)
.WithMany(s => s.Brands)
.HasForeignKey(cb => cb.SupplierId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using HotChocolate.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace HotChocolate.Data.Data.EntityConfigurations;

internal sealed class SupplierEntityTypeConfiguration : IEntityTypeConfiguration<Supplier>
{
public void Configure(EntityTypeBuilder<Supplier> builder)
{
builder
.ToTable("Suppliers");

builder
.Property(s => s.Name)
.HasMaxLength(100);

builder
.Property(s => s.Website)
.HasMaxLength(256);

builder
.Property(s => s.ContactEmail)
.HasMaxLength(256);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,33 @@ public async Task Query_Brands_With_BatchResolver_ProductCount()
MatchSnapshot(result, interceptor);
}

[Fact]
public async Task Query_Brands_With_BatchResolver_Supplier()
{
// arrange
using var interceptor = new TestQueryInterceptor();

// act
var result = await ExecuteAsync(
"""
{
brands(first: 5) {
nodes {
id
name
supplier {
name
website
}
}
}
}
""");

// assert
MatchSnapshot(result, interceptor);
}

[Fact]
public async Task Query_Brands_First_2_And_Products_First_2_Name_Desc()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,27 @@ public async Task SeedAsync(CatalogContext context)
var sourceJson = FileResource.Open("catalog.json");
var sourceItems = JsonSerializer.Deserialize<ProductEntry[]>(sourceJson)!;

// Seed suppliers first (brands will reference them).
context.Suppliers.RemoveRange(context.Suppliers);
var suppliers = new[]
{
new Supplier { Name = "Global Supply Co.", Website = "https://globalsupply.example.com", ContactEmail = "info@globalsupply.example.com" },
new Supplier { Name = "Prime Distribution", Website = "https://primedist.example.com", ContactEmail = "sales@primedist.example.com" },
new Supplier { Name = "Atlas Logistics", Website = "https://atlaslogistics.example.com", ContactEmail = "contact@atlaslogistics.example.com" }
};
await context.Suppliers.AddRangeAsync(suppliers);
await context.SaveChangesAsync();

var supplierIds = await context.Suppliers.Select(s => s.Id).ToListAsync();

context.Brands.RemoveRange(context.Brands);
var brandNames = sourceItems.Select(x => x.Brand).Distinct().ToList();
await context.Brands.AddRangeAsync(
sourceItems.Select(x => x.Brand).Distinct().Select(brandName => new Brand { Name = brandName }));
brandNames.Select((brandName, i) => new Brand
{
Name = brandName,
SupplierId = supplierIds[i % supplierIds.Count]
}));

context.ProductTypes.RemoveRange(context.ProductTypes);
await context.ProductTypes.AddRangeAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ public sealed class Brand
[Required]
public string Name { get; set; } = null!;

public int SupplierId { get; set; }

public Supplier? Supplier { get; set; }

public ICollection<Product> Products { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;

namespace HotChocolate.Data.Models;

public sealed class Supplier
{
public int Id { get; set; }

[Required]
public string Name { get; set; } = null!;

public string? Website { get; set; }

public string? ContactEmail { get; set; }

public ICollection<Brand> Brands { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,25 @@ public static async Task<List<int>> GetProductCountAsync(

return brands.Select(b => counts.GetValueOrDefault(b.Id, 0)).ToList();
}

[BindMember(nameof(Brand.SupplierId))]
[BatchResolver]
public static async Task<List<Supplier?>> GetSupplierAsync(
[Parent(requires: nameof(Brand.SupplierId))] List<Brand> brands,
QueryContext<Supplier> query,
[Service] CatalogContext context,
CancellationToken cancellationToken)
{
var supplierIds = brands.Select(b => b.SupplierId).Distinct().ToList();

var queryable = context.Suppliers
.Where(s => supplierIds.Contains(s.Id))
.With(query.Include(s => s.Id));
PagingQueryInterceptor.Publish(queryable);

var suppliers = await queryable
.ToDictionaryAsync(s => s.Id, cancellationToken);

return brands.Select(b => suppliers.GetValueOrDefault(b.SupplierId)).ToList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type BillingStatementTransaction implements StatementTransaction {
type Brand implements Node {
products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): BrandProductsConnection! @listSize(assumedSize: 50, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 10, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10")
productCount: Int! @cost(weight: "10")
supplier: Supplier @cost(weight: "10")
id: ID!
name: String!
}
Expand Down Expand Up @@ -182,6 +183,14 @@ type SingleProperty {
id: String!
}

type Supplier {
id: Int!
name: String!
website: String
contactEmail: String
brands: [Brand!]!
}

input BooleanOperationFilterInput {
eq: Boolean @cost(weight: "10")
neq: Boolean @cost(weight: "10")
Expand All @@ -192,6 +201,7 @@ input BrandFilterInput {
or: [BrandFilterInput!]
id: IntOperationFilterInput
name: StringOperationFilterInput
supplier: SupplierFilterInput
products: ListFilterInputTypeOfProductFilterInput
}

Expand Down Expand Up @@ -235,6 +245,13 @@ input IntOperationFilterInput {
nlte: Int @cost(weight: "10")
}

input ListFilterInputTypeOfBrandFilterInput {
all: BrandFilterInput @cost(weight: "10")
none: BrandFilterInput @cost(weight: "10")
some: BrandFilterInput @cost(weight: "10")
any: Boolean @cost(weight: "10")
}

input ListFilterInputTypeOfProductFilterInput {
all: ProductFilterInput @cost(weight: "10")
none: ProductFilterInput @cost(weight: "10")
Expand Down Expand Up @@ -289,6 +306,16 @@ input StringOperationFilterInput {
nendsWith: String @cost(weight: "20")
}

input SupplierFilterInput {
and: [SupplierFilterInput!]
or: [SupplierFilterInput!]
id: IntOperationFilterInput
name: StringOperationFilterInput
website: StringOperationFilterInput
contactEmail: StringOperationFilterInput
brands: ListFilterInputTypeOfBrandFilterInput
}

"Defines the possible serialization types for GraphQL scalar values."
enum ScalarSerializationType {
"The scalar serializes to a string value."
Expand Down Expand Up @@ -329,7 +356,7 @@ type User @internal {
id: ID!
name: String!
}

directive @internal on OBJECT | FIELD_DEFINITION
"""
directive @internal on OBJECT | FIELD_DEFINITION
Expand All @@ -350,11 +377,11 @@ directive @serializeAs("The primitive type a scalar is serialized to." type: [Sc
By default, only a single source schema is allowed to contribute
a particular field to an object type.


This prevents source schemas from inadvertently defining similarly named
fields that are not semantically equivalent.


Fields must be explicitly marked as @shareable to allow multiple source
schemas to define them, ensuring that the decision to serve a field from
more than one source schema is intentional and coordinated.
Expand Down
Loading
Loading