Skip to content

Commit 24cf5c5

Browse files
huysentruitwmichaelstaib
authored andcommitted
Use the new .NET api to check for nullable reference types (#7827)
1 parent 05e8213 commit 24cf5c5

11 files changed

+102
-19
lines changed

src/HotChocolate/Core/src/Execution/Projections/SelectionExpressionBuilder.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace HotChocolate.Execution.Projections;
1212

1313
internal sealed class SelectionExpressionBuilder
1414
{
15+
private static readonly NullabilityInfoContext _nullabilityInfoContext = new();
16+
1517
public Expression<Func<TRoot, TRoot>> BuildExpression<TRoot>(ISelection selection)
1618
{
1719
var rootType = typeof(TRoot);
@@ -293,14 +295,9 @@ private static bool IsNullableType(PropertyInfo propertyInfo)
293295
return Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
294296
}
295297

296-
var nullableAttribute = propertyInfo.GetCustomAttribute<NullableAttribute>();
297-
298-
if (nullableAttribute != null)
299-
{
300-
return nullableAttribute.NullableFlags[0] == 2;
301-
}
298+
var nullabilityInfo = _nullabilityInfoContext.Create(propertyInfo);
302299

303-
return false;
300+
return nullabilityInfo.WriteState == NullabilityState.Nullable;
304301
}
305302
#else
306303
private static bool IsNullableType(PropertyInfo propertyInfo)

src/HotChocolate/Core/test/Execution.Tests/Projections/ProjectableDataLoaderTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,39 @@ public async Task Project_Key_To_Collection_Expression_Integration()
849849
.MatchMarkdownSnapshot();
850850
}
851851

852+
[Fact]
853+
public async Task Product_With_Nullable_Reference_Property_Set_To_Null()
854+
{
855+
// Arrange
856+
var queries = new List<string>();
857+
var connectionString = CreateConnectionString();
858+
await CatalogContext.SeedAsync(connectionString);
859+
860+
// Act
861+
var result = await new ServiceCollection()
862+
.AddScoped(_ => queries)
863+
.AddTransient(_ => new CatalogContext(connectionString))
864+
.AddGraphQLServer()
865+
.AddQueryType<ProductsWithNullPropertyQuery>()
866+
.ExecuteRequestAsync(
867+
"""
868+
{
869+
productById(id: 1) {
870+
name
871+
type {
872+
name
873+
}
874+
}
875+
}
876+
""");
877+
878+
// Assert
879+
Snapshot.Create()
880+
.AddSql(queries)
881+
.Add(result, "Result")
882+
.MatchMarkdownSnapshot();
883+
}
884+
852885
public class Query
853886
{
854887
public async Task<Brand?> GetBrandByIdAsync(
@@ -926,6 +959,39 @@ public async Task<IEnumerable<Brand>> GetBrandByIdAsync(
926959
.ToListAsync(cancellationToken);
927960
}
928961

962+
public class ProductsWithNullPropertyQuery
963+
{
964+
public async Task<ProductProjection?> GetProductByIdAsync(
965+
int id,
966+
List<string> queries,
967+
ISelection selection,
968+
CatalogContext context,
969+
CancellationToken cancellationToken)
970+
{
971+
var query = context.Products
972+
.Where(p => p.Id == id)
973+
.Select(p => new ProductProjection // Cast to an object with nullable Type property
974+
{
975+
Name = p.Name,
976+
})
977+
.Select(selection.AsSelector<ProductProjection>());
978+
979+
lock (queries)
980+
{
981+
queries.Add(query.ToQueryString());
982+
}
983+
984+
return await query.SingleOrDefaultAsync(cancellationToken);
985+
}
986+
987+
public class ProductProjection
988+
{
989+
public string Name { get; set; } = default!;
990+
991+
public ProductType? Type { get; set; }
992+
}
993+
}
994+
929995
[ExtendObjectType<Brand>]
930996
public class BrandExtensions
931997
{

src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Branches_Are_Merged.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
```text
66
-- @__keys_0={ '1', '2' } (DbType = Object)
7-
SELECT p."Name", b."Name", p."Id"
7+
SELECT p."Name", FALSE, b."Name", p."Id"
88
FROM "Products" AS p
99
INNER JOIN "Brands" AS b ON p."BrandId" = b."Id"
1010
WHERE p."Id" = ANY (@__keys_0)

src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Force_A_Branch.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
```text
66
-- @__keys_0={ '1' } (DbType = Object)
7-
SELECT p."Name", b."Name", p."Id"
7+
SELECT p."Name", FALSE, b."Name", p."Id"
88
FROM "Products" AS p
99
INNER JOIN "Brands" AS b ON p."BrandId" = b."Id"
1010
WHERE p."Id" = ANY (@__keys_0)
1111
-- @__keys_0={ '1' } (DbType = Object)
12-
SELECT p."Id", b."Id"
12+
SELECT p."Id", FALSE, b."Id"
1313
FROM "Products" AS p
1414
INNER JOIN "Brands" AS b ON p."BrandId" = b."Id"
1515
WHERE p."Id" = ANY (@__keys_0)

src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Product_With_Name_And_Brand_With_Name.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
```text
66
-- @__keys_0={ '1' } (DbType = Object)
7-
SELECT p."Name", b."Name", p."Id"
7+
SELECT p."Name", FALSE, b."Name", p."Id"
88
FROM "Products" AS p
99
INNER JOIN "Brands" AS b ON p."BrandId" = b."Id"
1010
WHERE p."Id" = ANY (@__keys_0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Product_With_Nullable_Reference_Property_Set_To_Null
2+
3+
## SQL
4+
5+
```text
6+
-- @__id_0='1'
7+
SELECT p."Name"
8+
FROM "Products" AS p
9+
WHERE p."Id" = @__id_0
10+
```
11+
12+
## Result
13+
14+
```json
15+
{
16+
"data": {
17+
"productById": {
18+
"name": "Product 0-0",
19+
"type": null
20+
}
21+
}
22+
}
23+
```
24+

src/HotChocolate/Core/test/Execution.Tests/TestContext/Product.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ public class Product
3434
// Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses)
3535
public int MaxStockThreshold { get; set; }
3636

37-
/// <summary>Optional embedding for the catalog item's description.</summary>
38-
// [JsonIgnore]
39-
// public Vector Embedding { get; set; }
40-
4137
/// <summary>
4238
/// True if item is on reorder
4339
/// </summary>

src/HotChocolate/Pagination/test/Pagination.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ORDER BY t."Name", t."Id"
1818
## Expression 0
1919

2020
```text
21-
[Microsoft.EntityFrameworkCore.Query.QueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = IIF((root.Name == null), null, root.Name), Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = IIF((root.Bar.Description == null), null, root.Bar.Description)})}).Take(11)
21+
[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = root.Name, Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = IIF((root.Bar.Description == null), null, root.Bar.Description)})}).Take(11)
2222
```
2323

2424
## Result

src/HotChocolate/Pagination/test/Pagination.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ORDER BY t."Name", t."Id"
1818
## Expression 0
1919

2020
```text
21-
[Microsoft.EntityFrameworkCore.Query.QueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = IIF((root.Name == null), null, root.Name), Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = IIF((root.Bar.Description == null), null, root.Bar.Description), SomeField1 = IIF((root.Bar.SomeField1 == null), null, root.Bar.SomeField1), SomeField2 = IIF((root.Bar.SomeField2 == null), null, root.Bar.SomeField2)})}).Take(11)
21+
[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = root.Name, Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = IIF((root.Bar.Description == null), null, root.Bar.Description), SomeField1 = root.Bar.SomeField1, SomeField2 = IIF((root.Bar.SomeField2 == null), null, root.Bar.SomeField2)})}).Take(11)
2222
```
2323

2424
## Result

src/HotChocolate/Pagination/test/Pagination.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2_NET9_0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ORDER BY f0."Name", f0."Id"
1818
## Expression 0
1919

2020
```text
21-
[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = root.Name, Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = root.Bar.Description, SomeField1 = root.Bar.SomeField1, SomeField2 = root.Bar.SomeField2})}).Take(11)
21+
[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => new Foo() {Id = root.Id, Name = root.Name, Bar = IIF((root.Bar == null), null, new Bar() {Id = root.Bar.Id, Description = IIF((root.Bar.Description == null), null, root.Bar.Description), SomeField1 = root.Bar.SomeField1, SomeField2 = IIF((root.Bar.SomeField2 == null), null, root.Bar.SomeField2)})}).Take(11)
2222
```
2323

2424
## Result

0 commit comments

Comments
 (0)