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
106 changes: 13 additions & 93 deletions src/GreenDonut/src/Core/Projections/SelectionDataLoaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public static class SelectionDataLoaderExtensions
/// <exception cref="ArgumentNullException">
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
/// </exception>
public static ISelectionDataLoader<TKey, TValue> Select<TKey, TValue>(
public static IDataLoader<TKey, TValue> Select<TKey, TValue>(
this IDataLoader<TKey, TValue> dataLoader,
Expression<Func<TValue, TValue>> selector)
Expression<Func<TValue, TValue>>? selector)
where TKey : notnull
{
if (dataLoader is null)
Expand All @@ -44,7 +44,14 @@ public static ISelectionDataLoader<TKey, TValue> Select<TKey, TValue>(

if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
return dataLoader;
}

if (dataLoader is ISelectionDataLoader<TKey, TValue>)
{
var context = (DefaultSelectorBuilder<TValue>)dataLoader.ContextData[typeof(ISelectorBuilder).FullName!]!;
context.Add(selector);
return dataLoader;
}

var branchKey = selector.ToString();
Expand All @@ -65,48 +72,6 @@ static IDataLoader CreateBranch(
}
}

/// <summary>
/// Adds another selector to the branched DataLoader.
/// </summary>
/// <param name="dataLoader">
/// The branched DataLoader.
/// </param>
/// <param name="selector">
/// The data selector.
/// </param>
/// <typeparam name="TKey">
/// The key type.
/// </typeparam>
/// <typeparam name="TValue">
/// The value type.
/// </typeparam>
/// <returns>
/// Returns the branched DataLoader with the selector applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
/// </exception>
public static ISelectionDataLoader<TKey, TValue> Select<TKey, TValue>(
this ISelectionDataLoader<TKey, TValue> dataLoader,
Expression<Func<TValue, TValue>> selector)
where TKey : notnull
{
if (dataLoader is null)
{
throw new ArgumentNullException(nameof(dataLoader));
}

if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}

var context = (DefaultSelectorBuilder<TValue>)dataLoader.ContextData[typeof(ISelectorBuilder).FullName!]!;
context.Add(selector);
return dataLoader;
}

/*
/// <summary>
/// Includes a property in the query.
/// </summary>
Expand Down Expand Up @@ -161,54 +126,9 @@ public static IDataLoader<TKey, TValue> Include<TKey, TValue>(
nameof(includeSelector));
}

DefaultSelectorBuilder<TValue> context;
if (dataLoader.ContextData.TryGetValue(typeof(ISelectorBuilder).FullName!, out var value)
&& value is DefaultSelectorBuilder<TValue> casted)
{
context = casted;
}
else
{
context = new DefaultSelectorBuilder<TValue>();
}

context.Add(ExpressionHelpers.Rewrite(includeSelector));
dataLoader.ContextData = dataLoader.ContextData.SetItem(typeof(ISelectorBuilder).FullName!, context);
return dataLoader;
}
*/

public static ISelectionDataLoader<TKey, TValue> Include<TKey, TValue>(
this ISelectionDataLoader<TKey, TValue> dataLoader,
Expression<Func<TValue, object?>> includeSelector)
where TKey : notnull
{
if (dataLoader is null)
{
throw new ArgumentNullException(nameof(dataLoader));
}

if (includeSelector is null)
{
throw new ArgumentNullException(nameof(includeSelector));
}

if (includeSelector is not LambdaExpression lambda)
{
throw new ArgumentException(
"The include selector must be a lambda expression.",
nameof(includeSelector));
}

if (lambda.Body is not MemberExpression member
|| member.Member.MemberType != MemberTypes.Property)
{
throw new ArgumentException(
"The include selector must be a property selector.",
nameof(includeSelector));
}

var context = (DefaultSelectorBuilder<TValue>)dataLoader.ContextData[typeof(ISelectorBuilder).FullName!]!;
var context = dataLoader.GetOrSetState(
typeof(ISelectorBuilder).FullName!,
_ => new DefaultSelectorBuilder<TValue>());
context.Add(Rewrite(includeSelector));
return dataLoader;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static class HotChocolateExecutionDataLoaderExtensions
/// <returns>
/// Returns a new data loader that applies the selection.
/// </returns>
public static ISelectionDataLoader<TKey, TValue> Select<TKey, TValue>(
public static IDataLoader<TKey, TValue> Select<TKey, TValue>(
this IDataLoader<TKey, TValue> dataLoader,
ISelection selection)
where TKey : notnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,26 @@ IObjectFieldDescriptor Directive<T>()
/// <param name="arguments">The arguments of the directive</param>
/// <returns>The descriptor</returns>
IObjectFieldDescriptor Directive(string name, params ArgumentNode[] arguments);

/// <summary>
/// Specifies the requirements for the parent object.
/// </summary>
/// <param name="requires">
/// The requirements for the parent object.
/// </param>
/// <returns>
/// Returns the descriptor to chain further configuration.
/// </returns>
IObjectFieldDescriptor ParentRequires<TParent>(string? requires);

/// <summary>
/// Specifies the requirements for the parent object.
/// </summary>
/// <param name="requires">
/// The requirements for the parent object.
/// </param>
/// <returns>
/// Returns the descriptor to chain further configuration.
/// </returns>
IObjectFieldDescriptor ParentRequires(string? requires);
}
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,39 @@ public IObjectFieldDescriptor Use(FieldMiddleware middleware)
return this;
}

/// <inheritdoc />
public IObjectFieldDescriptor ParentRequires<TParent>(string? requires)
{
if (!(requires?.Length > 0))
{
Definition.Flags &= ~FieldFlags.WithRequirements;
Definition.ContextData.Remove(FieldRequirementsSyntax);
Definition.ContextData.Remove(FieldRequirementsEntity);
return this;
}

Definition.Flags |= FieldFlags.WithRequirements;
Definition.ContextData[FieldRequirementsSyntax] = requires;
Definition.ContextData[FieldRequirementsEntity] = typeof(TParent);
return this;
}

public IObjectFieldDescriptor ParentRequires(string? requires)
{
if (!(requires?.Length > 0))
{
Definition.Flags &= ~FieldFlags.WithRequirements;
Definition.ContextData.Remove(FieldRequirementsSyntax);
Definition.ContextData.Remove(FieldRequirementsEntity);
return this;
}

Definition.Flags |= FieldFlags.WithRequirements;
Definition.ContextData[FieldRequirementsSyntax] = requires;
Definition.ContextData[FieldRequirementsEntity] = Definition.SourceType;
return this;
}

/// <summary>
/// Creates a new instance of <see cref="ObjectFieldDescriptor"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq.Expressions;
using CookieCrumble;
using GreenDonut;
using GreenDonut.Projections;
Expand Down Expand Up @@ -49,6 +50,37 @@ public async Task Brand_With_Name()
.MatchMarkdownSnapshot();
}

[Fact]
public async Task Brand_With_Name_Selector_is_Null()
{
// Arrange
var queries = new List<string>();
var connectionString = CreateConnectionString();
await CatalogContext.SeedAsync(connectionString);

// Act
var result = await new ServiceCollection()
.AddScoped(_ => queries)
.AddTransient(_ => new CatalogContext(connectionString))
.AddGraphQL()
.AddQueryType<Query>()
.AddPagingArguments()
.ModifyRequestOptions(o => o.IncludeExceptionDetails = true)
.ExecuteRequestAsync(
"""
{
brandByIdSelectorNull(id: 1) {
name
}
}
""");

Snapshot.Create()
.AddSql(queries)
.AddResult(result)
.MatchMarkdownSnapshot();
}

[Fact]
public async Task Manual_Include_Of_Brand()
{
Expand Down Expand Up @@ -399,6 +431,70 @@ public async Task Brand_Details_Requires_Brand_Name()
.MatchMarkdownSnapshot();
}

[Fact]
public async Task Brand_Details_Requires_Brand_Name_With_Proper_Type()
{
// Arrange
var queries = new List<string>();
var connectionString = CreateConnectionString();
await CatalogContext.SeedAsync(connectionString);

// Act
var result = await new ServiceCollection()
.AddScoped(_ => queries)
.AddTransient(_ => new CatalogContext(connectionString))
.AddGraphQL()
.AddQueryType<Query>()
.AddType<BrandWithRequirementType>()
.AddPagingArguments()
.ModifyRequestOptions(o => o.IncludeExceptionDetails = true)
.ExecuteRequestAsync(
"""
{
brandById(id: 1) {
details
}
}
""");

Snapshot.Create()
.AddSql(queries)
.AddResult(result)
.MatchMarkdownSnapshot();
}

[Fact]
public async Task Brand_Details_Requires_Brand_Name_With_Proper_Type_With_Explicit_Generic()
{
// Arrange
var queries = new List<string>();
var connectionString = CreateConnectionString();
await CatalogContext.SeedAsync(connectionString);

// Act
var result = await new ServiceCollection()
.AddScoped(_ => queries)
.AddTransient(_ => new CatalogContext(connectionString))
.AddGraphQL()
.AddQueryType<Query>()
.AddType<BrandWithRequirementTypeWithGeneric>()
.AddPagingArguments()
.ModifyRequestOptions(o => o.IncludeExceptionDetails = true)
.ExecuteRequestAsync(
"""
{
brandById(id: 1) {
details
}
}
""");

Snapshot.Create()
.AddSql(queries)
.AddResult(result)
.MatchMarkdownSnapshot();
}

[Fact]
public async Task Brand_Details_Requires_Brand_Name_2()
{
Expand Down Expand Up @@ -460,7 +556,7 @@ public async Task Brand_Products_TypeName()
""");

// at the moment we do not support projections on lists
// so products will be empty and we will just select the brand.Id
// so products will be empty, and we will just select the brand.Id
Snapshot.Create()
.AddSql(queries)
.AddResult(result)
Expand Down Expand Up @@ -529,14 +625,18 @@ public class Query
CancellationToken cancellationToken)
=> await productById.Select(selection).Include(c => c.Brand).LoadAsync(id, cancellationToken);

/*
public async Task<Product?> GetProductByIdWithBrandNoSelectionAsync(
int id,
ISelection selection,
ProductByIdDataLoader productById,
CancellationToken cancellationToken)
=> await productById.Include(c => c.Brand).LoadAsync(id, cancellationToken);
*/

public async Task<Brand?> GetBrandByIdSelectorNullAsync(
int id,
BrandByIdDataLoader brandById,
CancellationToken cancellationToken)
=> await brandById.Select(default(Expression<Func<Brand, Brand>>)).LoadAsync(id, cancellationToken);
}

[ExtendObjectType<Brand>]
Expand All @@ -557,6 +657,28 @@ public string GetDetails(
=> "Brand Name:" + brand.Name;
}

public class BrandWithRequirementType : ObjectType<Brand>
{
protected override void Configure(IObjectTypeDescriptor<Brand> descriptor)
{
descriptor
.Field(t => t.Details)
.ParentRequires(nameof(Brand.Name))
.Resolve(ctx => "Brand Name:" + ctx.Parent<Brand>().Name);
}
}

public class BrandWithRequirementTypeWithGeneric : ObjectType<Brand>
{
protected override void Configure(IObjectTypeDescriptor<Brand> descriptor)
{
descriptor
.Field(t => t.Details)
.ParentRequires<Brand>(nameof(Brand.Name))
.Resolve(ctx => "Brand Name:" + ctx.Parent<Brand>().Name);
}
}

[ExtendObjectType<Product>]
public class ProductExtensions
{
Expand Down
Loading