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
19 changes: 18 additions & 1 deletion docs/mdsource/query-usage.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ To negate an expression, including a group, wrap it in `not`:

#### Nested Properties

A reference navigation, owned type or complex property is a nested where object. No null checking of nested values is done.
A reference navigation, owned type or complex property is a nested where object. No null checking of nested values is done; to test the navigation itself see [Null](#null).

```graphql
{
Expand Down Expand Up @@ -263,6 +263,23 @@ Null is compared with a null value:

An empty where, `{}`, applies no filter.

A reference navigation is tested with `isNull`, since a nested where can only constrain the members
of the navigation, not the navigation itself:

```graphql
{
entities
(where: {address: {isNull: true}})
{
property
}
}
```

`isNull: false` requires the navigation to be present. A collection navigation uses `none: {}` and
`any: {}` instead. `isNull` has no meaning at the root of a where, since there is no navigation it
was reached through.


#### Variables

Expand Down
34 changes: 33 additions & 1 deletion docs/mdsource/upgradeGuide35.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ From [#1377](https://github.com/SimonCropp/GraphQL.EntityFramework/pull/1377). T
* The extension methods `Resolve`, `ResolveAsync`, `ResolveList` and `ResolveListAsync` take the `IEfGraphQLService<TDbContext>` as their first argument. All type arguments are inferred, and the service is used directly at execution time instead of being located through `RequestServices`.
* `FieldBuilderResolveAnalyzer` previously never matched the extension methods, so GQLEF003 could not fire. It now resolves the receiver type for extension methods and identifies projection based calls by their `projection` parameter.

The old extension methods and the `LambdaExpression` overload of `WithProjection` are removed rather than kept as overloads.
The old extension methods are removed rather than kept as overloads. The `LambdaExpression` overload
of `WithProjection` was removed in 35.0.0 and restored in 35.1.0, for a projection built by
reflection or by `Expression.Lambda`, where the caller does not have the source and projection types.


### WithProjection
Expand Down Expand Up @@ -111,6 +113,7 @@ input PersonWhere {
and: [PersonWhere!]
or: [PersonWhere!]
not: PersonWhere
isNull: Boolean
id: GuidComparison
name: StringComparison
age: Int32Comparison
Expand Down Expand Up @@ -538,6 +541,35 @@ After, pass null:

An empty where, `{}`, applies no filter. The old empty list, `[]`, matched nothing.

A path that named a reference navigation rather than one of its properties tested the navigation
itself. The nested where has no comparison of its own, so use `isNull` (added in 35.1.0).

Before:

```graphql
{
entities (where: {path: "Address", comparison: equal})
{
property
}
}
```

After:

```graphql
{
entities (where: {address: {isNull: true}})
{
property
}
}
```

`isNull: false` requires the navigation to be present, as `comparison: notEqual` did. A collection
navigation uses `none: {}` and `any: {}` instead, and `isNull` is rejected at the root of a where,
where there is no navigation it was reached through.


### Variables

Expand Down
19 changes: 18 additions & 1 deletion docs/query-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ To negate an expression, including a group, wrap it in `not`:

#### Nested Properties

A reference navigation, owned type or complex property is a nested where object. No null checking of nested values is done.
A reference navigation, owned type or complex property is a nested where object. No null checking of nested values is done; to test the navigation itself see [Null](#null).

```graphql
{
Expand Down Expand Up @@ -270,6 +270,23 @@ Null is compared with a null value:

An empty where, `{}`, applies no filter.

A reference navigation is tested with `isNull`, since a nested where can only constrain the members
of the navigation, not the navigation itself:

```graphql
{
entities
(where: {address: {isNull: true}})
{
property
}
}
```

`isNull: false` requires the navigation to be present. A collection navigation uses `none: {}` and
`any: {}` instead. `isNull` has no meaning at the root of a where, since there is no navigation it
was reached through.


#### Variables

Expand Down
34 changes: 33 additions & 1 deletion docs/upgradeGuide35.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ From [#1377](https://github.com/SimonCropp/GraphQL.EntityFramework/pull/1377). T
* The extension methods `Resolve`, `ResolveAsync`, `ResolveList` and `ResolveListAsync` take the `IEfGraphQLService<TDbContext>` as their first argument. All type arguments are inferred, and the service is used directly at execution time instead of being located through `RequestServices`.
* `FieldBuilderResolveAnalyzer` previously never matched the extension methods, so GQLEF003 could not fire. It now resolves the receiver type for extension methods and identifies projection based calls by their `projection` parameter.

The old extension methods and the `LambdaExpression` overload of `WithProjection` are removed rather than kept as overloads.
The old extension methods are removed rather than kept as overloads. The `LambdaExpression` overload
of `WithProjection` was removed in 35.0.0 and restored in 35.1.0, for a projection built by
reflection or by `Expression.Lambda`, where the caller does not have the source and projection types.


### WithProjection
Expand Down Expand Up @@ -118,6 +120,7 @@ input PersonWhere {
and: [PersonWhere!]
or: [PersonWhere!]
not: PersonWhere
isNull: Boolean
id: GuidComparison
name: StringComparison
age: Int32Comparison
Expand Down Expand Up @@ -545,6 +548,35 @@ After, pass null:

An empty where, `{}`, applies no filter. The old empty list, `[]`, matched nothing.

A path that named a reference navigation rather than one of its properties tested the navigation
itself. The nested where has no comparison of its own, so use `isNull` (added in 35.1.0).

Before:

```graphql
{
entities (where: {path: "Address", comparison: equal})
{
property
}
}
```

After:

```graphql
{
entities (where: {address: {isNull: true}})
{
property
}
}
```

`isNull: false` requires the navigation to be present, as `comparison: notEqual` did. A collection
navigation uses `none: {}` and `any: {}` instead, and `isNull` is rejected at the root of a where,
where there is no navigation it was reached through.


### Variables

Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU5104;CS1573;CS9107;NU1608;NU1109;SC023</NoWarn>
<Version>35.0.0</Version>
<Version>35.1.0</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
21 changes: 20 additions & 1 deletion src/GraphQL.EntityFramework/GraphApi/FieldBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,26 @@ public static FieldBuilder<TSource, TReturn> WithProjection<TSource, TReturn, TP
return builder;
}

static void ValidateProjection<TSource, TProjection>(Expression<Func<TSource, TProjection>> projection)
/// <summary>
/// Sets projection metadata on a field from a projection whose type is only known at runtime.
/// Prefer the <see cref="Expression{TDelegate}"/> overload, which infers its type arguments and
/// is checked at compile time. This one is for a projection built by reflection or by
/// <see cref="Expression.Lambda(Expression,ParameterExpression[])"/>, where the source and
/// projection types are not available to the caller.
/// </summary>
/// <param name="builder">The field builder</param>
/// <param name="projection">Expression describing the required entity data. Its parameter must be <typeparamref name="TSource"/> or a type it derives from.</param>
/// <returns>The field builder for chaining</returns>
public static FieldBuilder<TSource, TReturn> WithProjection<TSource, TReturn>(
this FieldBuilder<TSource, TReturn> builder,
LambdaExpression projection)
{
ValidateProjection(projection);
IncludeAppender.SetProjectionMetadata(builder.FieldType, projection);
return builder;
}

static void ValidateProjection(LambdaExpression projection)
{
// Detect identity projection: _ => _
if (projection.Body is ParameterExpression parameter &&
Expand Down
7 changes: 7 additions & 0 deletions src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ public static Expression<Func<T, bool>> BuildPredicate(WhereExpression where) =>

static Expression MakePredicateBody(string path, Comparison comparison, object?[]? values, bool negate)
{
if (path.Length == 0)
{
// isNull tests the navigation a nested where was reached through, so there is nothing
// to test at the root of a where.
throw new($"isNull is only valid on a navigation. It was used at the root of the where for {typeof(T).Name}.");
}

try
{
Expression expressionBody;
Expand Down
21 changes: 18 additions & 3 deletions src/GraphQL.EntityFramework/Where/Graphs/WhereGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ namespace GraphQL.EntityFramework;
/// <summary>
/// The where input type for <typeparamref name="TEntity"/>. One field per mapped property,
/// typed by its comparisons, one per navigation, and <c>and</c>, <c>or</c> and <c>not</c> to
/// compose them. Sibling fields are and'ed. The parsed value is a <see cref="WhereExpression"/>
/// tree, so the predicate builder is unchanged.
/// compose them. Sibling fields are and'ed. <c>isNull</c> tests the navigation this where was
/// reached through, which is the one thing a nested where cannot say with its own fields; it has
/// no meaning at the root. The parsed value is a <see cref="WhereExpression"/> tree, so the
/// predicate builder is unchanged.
/// </summary>
public class WhereGraph<TEntity> :
InputObjectGraphType,
Expand Down Expand Up @@ -55,6 +57,11 @@ public WhereGraph(IEnumerable<IEfGraphQLService> services)
Name = "not",
Type = self
});
AddField(new()
{
Name = "isNull",
Type = typeof(BooleanGraphType)
});

foreach (var member in EntityShape.Members(type, services))
{
Expand Down Expand Up @@ -104,6 +111,14 @@ public override object ParseDictionary(IDictionary<string, object?> value)
expressions.Add(negated);
}

continue;
case "isNull":
// The navigation itself, rather than a member of it. Prefix turns the empty
// path into the navigation's name.
expressions.Add(new()
{
Comparison = (bool) raw ? Comparison.Equal : Comparison.NotEqual
});
continue;
}

Expand Down Expand Up @@ -219,6 +234,6 @@ static void Prefix(WhereExpression expression, string name)
return;
}

expression.Path = $"{name}.{expression.Path}";
expression.Path = expression.Path.Length == 0 ? name : $"{name}.{expression.Path}";
}
}
7 changes: 7 additions & 0 deletions src/GraphQL.EntityFramework/Where/TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ static bool ParseBoolean(string value) =>

if (value is null)
{
// Null is a meaningful comparison for a reference typed member: a navigation with no
// related row. Only a non nullable value type has nothing to compare against.
if (!type.IsValueType)
{
return null;
}

throw new($"Null passed for non nullable type '{type.FullName}'.");
}

Expand Down
3 changes: 3 additions & 0 deletions src/SampleWeb.Tests/SchemaPrint.Print.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ input EmployeeWhere {
and: [EmployeeWhere!]
or: [EmployeeWhere!]
not: EmployeeWhere
isNull: Boolean
age: Int32Comparison
company: CompanyWhere
companyId: Int32Comparison
Expand All @@ -91,6 +92,7 @@ input CompanyWhere {
and: [CompanyWhere!]
or: [CompanyWhere!]
not: CompanyWhere
isNull: Boolean
content: StringComparison
employees: EmployeeCollectionWhere
id: Int32Comparison
Expand Down Expand Up @@ -122,6 +124,7 @@ input DeviceWhere {
and: [DeviceWhere!]
or: [DeviceWhere!]
not: DeviceWhere
isNull: Boolean
employees: EmployeeCollectionWhere
id: Int32Comparison
name: StringComparison
Expand Down
Loading
Loading