Skip to content

Commit b87ef47

Browse files
committed
Fix to #21006 - Support a default value for non-nullable properties
Only for scalar properties when projecting Json-mapped entity. Only need to change code for Cosmos - relational already works in the desired way after the change to streaming (properties that are not encountered maintain their default value) We still throw exception if JSON contains explicit null where non-nullable scalar is expected. Fixes #21006
1 parent 34ee81a commit b87ef47

File tree

8 files changed

+860
-12
lines changed

8 files changed

+860
-12
lines changed

Diff for: src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs

+38-5
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ private Expression CreateGetValueExpression(
691691
&& !property.IsShadowProperty())
692692
{
693693
var readExpression = CreateGetValueExpression(
694-
jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping());
694+
jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping(), !property.IsNullable);
695695

696696
var nonNullReadExpression = readExpression;
697697
if (nonNullReadExpression.Type != type)
@@ -712,15 +712,23 @@ private Expression CreateGetValueExpression(
712712
}
713713

714714
return Convert(
715-
CreateGetValueExpression(jTokenExpression, storeName, type.MakeNullable(), property.GetTypeMapping()),
715+
CreateGetValueExpression(
716+
jTokenExpression,
717+
storeName,
718+
type.MakeNullable(),
719+
property.GetTypeMapping(),
720+
isNonNullableScalar: !property.IsNullable && !property.IsKey()),
716721
type);
717722
}
718723

724+
// private readonly static PropertyInfo JTokenTypePropertyInfo = typeof(JToken).GetProperty(nameof(JToken.Type))!;
725+
719726
private Expression CreateGetValueExpression(
720727
Expression jTokenExpression,
721728
string storeName,
722729
Type type,
723-
CoreTypeMapping typeMapping = null)
730+
CoreTypeMapping typeMapping = null,
731+
bool isNonNullableScalar = false)
724732
{
725733
Check.DebugAssert(type.IsNullableType(), "Must read nullable type from JObject.");
726734

@@ -763,6 +771,23 @@ var body
763771
Constant(CosmosClientWrapper.Serializer)),
764772
converter.ConvertFromProviderExpression.Body);
765773

774+
//if (isNonNullableScalar)
775+
//{
776+
// //same logic as SafeToObjectWithSerializer
777+
// //token == null || token.Type == JTokenType.Null ? default : body
778+
// body = Expression.Condition(
779+
// Expression.OrElse(
780+
// Expression.Equal(
781+
// jTokenParameter,
782+
// Expression.Constant(null, typeof(JToken))),
783+
// Expression.Equal(
784+
// Expression.Property(jTokenParameter, JTokenTypePropertyInfo),
785+
// Expression.Constant(JTokenType.Null))),
786+
// Expression.Default(body.Type),
787+
// body);
788+
//}
789+
790+
var originalBodyType = body.Type;
766791
if (body.Type != type)
767792
{
768793
body = Convert(body, type);
@@ -783,7 +808,11 @@ var body
783808
}
784809
else
785810
{
786-
replaceExpression = Default(type);
811+
replaceExpression = isNonNullableScalar
812+
? Expression.Convert(
813+
Default(originalBodyType),
814+
type)
815+
: Default(type);
787816
}
788817

789818
body = Condition(
@@ -799,7 +828,11 @@ var body
799828
}
800829
else
801830
{
802-
valueExpression = ConvertJTokenToType(jTokenExpression, typeMapping?.ClrType.MakeNullable() ?? type);
831+
valueExpression = ConvertJTokenToType(
832+
jTokenExpression,
833+
(isNonNullableScalar
834+
? typeMapping?.ClrType
835+
: typeMapping?.ClrType.MakeNullable()) ?? type);
803836

804837
if (valueExpression.Type != type)
805838
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net;
5+
using Microsoft.Azure.Cosmos;
6+
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
10+
namespace Microsoft.EntityFrameworkCore.Query;
11+
12+
public class AdHocCosmosTestHelpers
13+
{
14+
public static async Task CreateCustomEntityHelperAsync(
15+
Container container,
16+
string json,
17+
CancellationToken cancellationToken)
18+
{
19+
var document = JObject.Parse(json);
20+
21+
var stream = new MemoryStream();
22+
await using var __ = stream.ConfigureAwait(false);
23+
var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: false);
24+
await using var ___ = writer.ConfigureAwait(false);
25+
using var jsonWriter = new JsonTextWriter(writer);
26+
27+
CosmosClientWrapper.Serializer.Serialize(jsonWriter, document);
28+
await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
29+
30+
var response = await container.CreateItemStreamAsync(
31+
stream,
32+
PartitionKey.None,
33+
requestOptions: null,
34+
cancellationToken)
35+
.ConfigureAwait(false);
36+
37+
38+
if (response.StatusCode != HttpStatusCode.Created)
39+
{
40+
throw new InvalidOperationException($"Failed to create entitty (status code: {response.StatusCode}) for json: {json}");
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)