Skip to content

Commit d94cc5a

Browse files
authored
Add back support for Cosmos nested dictionaries (#34312)
* Add back support for Cosmos nested dictionaries Fixes #34105 This code will be consolidated with the relational UTF8 JSON code when #29825 is implemented. For now, just adding back what was already in Cosmos. * Review updates
1 parent e667bf8 commit d94cc5a

File tree

8 files changed

+406
-103
lines changed

8 files changed

+406
-103
lines changed

Diff for: src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public sealed class NullableStringDictionaryComparer<TElement, TCollection> : Va
1919
/// any release. You should only use it directly in your code with extreme caution and knowing that
2020
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2121
/// </summary>
22-
public NullableStringDictionaryComparer(ValueComparer elementComparer, bool readOnly)
22+
public NullableStringDictionaryComparer(ValueComparer elementComparer)
2323
: base(
2424
(a, b) => Compare(a, b, (ValueComparer<TElement>)elementComparer),
2525
o => GetHashCode(o, (ValueComparer<TElement>)elementComparer),
26-
source => Snapshot(source, (ValueComparer<TElement>)elementComparer, readOnly))
26+
source => Snapshot(source, (ValueComparer<TElement>)elementComparer))
2727
{
2828
}
2929

@@ -92,13 +92,8 @@ private static int GetHashCode(TCollection source, ValueComparer<TElement> eleme
9292
return hash.ToHashCode();
9393
}
9494

95-
private static TCollection Snapshot(TCollection source, ValueComparer<TElement> elementComparer, bool readOnly)
95+
private static TCollection Snapshot(TCollection source, ValueComparer<TElement> elementComparer)
9696
{
97-
if (readOnly)
98-
{
99-
return source;
100-
}
101-
10297
var snapshot = new Dictionary<string, TElement?>(((IReadOnlyDictionary<string, TElement?>)source).Count);
10398
foreach (var (key, element) in source)
10499
{
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections;
5+
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
6+
47
namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
58

69
/// <summary>
@@ -9,21 +12,30 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
912
/// any release. You should only use it directly in your code with extreme caution and knowing that
1013
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1114
/// </summary>
12-
public sealed class StringDictionaryComparer<TElement, TCollection> : ValueComparer<TCollection>
13-
where TCollection : class, IEnumerable<KeyValuePair<string, TElement>>
15+
public sealed class StringDictionaryComparer<TDictionary, TElement> : ValueComparer<object>, IInfrastructure<ValueComparer>
1416
{
17+
private static readonly MethodInfo CompareMethod = typeof(StringDictionaryComparer<TDictionary, TElement>).GetMethod(
18+
nameof(Compare), BindingFlags.Static | BindingFlags.NonPublic, [typeof(object), typeof(object), typeof(ValueComparer)])!;
19+
20+
private static readonly MethodInfo GetHashCodeMethod = typeof(StringDictionaryComparer<TDictionary, TElement>).GetMethod(
21+
nameof(GetHashCode), BindingFlags.Static | BindingFlags.NonPublic, [typeof(IEnumerable), typeof(ValueComparer)])!;
22+
23+
private static readonly MethodInfo SnapshotMethod = typeof(StringDictionaryComparer<TDictionary, TElement>).GetMethod(
24+
nameof(Snapshot), BindingFlags.Static | BindingFlags.NonPublic, [typeof(object), typeof(ValueComparer)])!;
25+
1526
/// <summary>
1627
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1728
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1829
/// any release. You should only use it directly in your code with extreme caution and knowing that
1930
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2031
/// </summary>
21-
public StringDictionaryComparer(ValueComparer elementComparer, bool readOnly)
32+
public StringDictionaryComparer(ValueComparer elementComparer)
2233
: base(
23-
(a, b) => Compare(a, b, (ValueComparer<TElement>)elementComparer),
24-
o => GetHashCode(o, (ValueComparer<TElement>)elementComparer),
25-
source => Snapshot(source, (ValueComparer<TElement>)elementComparer, readOnly))
34+
CompareLambda(elementComparer),
35+
GetHashCodeLambda(elementComparer),
36+
SnapshotLambda(elementComparer))
2637
{
38+
ElementComparer = elementComparer;
2739
}
2840

2941
/// <summary>
@@ -32,63 +44,136 @@ public StringDictionaryComparer(ValueComparer elementComparer, bool readOnly)
3244
/// any release. You should only use it directly in your code with extreme caution and knowing that
3345
/// doing so can result in application failures when updating to a new Entity Framework Core release.
3446
/// </summary>
35-
public override Type Type
36-
=> typeof(TCollection);
47+
public ValueComparer ElementComparer { get; }
48+
49+
ValueComparer IInfrastructure<ValueComparer>.Instance => ElementComparer;
50+
51+
private static Expression<Func<object?, object?, bool>> CompareLambda(ValueComparer elementComparer)
52+
{
53+
var prm1 = Expression.Parameter(typeof(object), "a");
54+
var prm2 = Expression.Parameter(typeof(object), "b");
55+
56+
return Expression.Lambda<Func<object?, object?, bool>>(
57+
Expression.Call(
58+
CompareMethod,
59+
prm1,
60+
prm2,
61+
#pragma warning disable EF9100
62+
elementComparer.ConstructorExpression),
63+
#pragma warning restore EF9100
64+
prm1,
65+
prm2);
66+
}
67+
68+
private static Expression<Func<object, int>> GetHashCodeLambda(ValueComparer elementComparer)
69+
{
70+
var prm = Expression.Parameter(typeof(object), "o");
71+
72+
return Expression.Lambda<Func<object, int>>(
73+
Expression.Call(
74+
GetHashCodeMethod,
75+
Expression.Convert(
76+
prm,
77+
typeof(IEnumerable)),
78+
#pragma warning disable EF9100
79+
elementComparer.ConstructorExpression),
80+
#pragma warning restore EF9100
81+
prm);
82+
}
83+
84+
private static Expression<Func<object, object>> SnapshotLambda(ValueComparer elementComparer)
85+
{
86+
var prm = Expression.Parameter(typeof(object), "source");
3787

38-
private static bool Compare(TCollection? a, TCollection? b, ValueComparer<TElement> elementComparer)
88+
return Expression.Lambda<Func<object, object>>(
89+
Expression.Call(
90+
SnapshotMethod,
91+
prm,
92+
#pragma warning disable EF9100
93+
elementComparer.ConstructorExpression),
94+
#pragma warning restore EF9100
95+
prm);
96+
}
97+
98+
private static bool Compare(object? a, object? b, ValueComparer elementComparer)
3999
{
40-
if (a is not IReadOnlyDictionary<string, TElement> aDict)
100+
if (ReferenceEquals(a, b))
41101
{
42-
return b is not IReadOnlyDictionary<string, TElement>;
102+
return true;
43103
}
44104

45-
if (b is not IReadOnlyDictionary<string, TElement> bDict || aDict.Count != bDict.Count)
105+
if (a is null)
46106
{
47-
return false;
107+
return b is null;
48108
}
49109

50-
if (ReferenceEquals(aDict, bDict))
110+
if (b is null)
51111
{
52-
return true;
112+
return false;
53113
}
54114

55-
foreach (var (key, element) in aDict)
115+
if (a is IReadOnlyDictionary<string, TElement?> aDictionary && b is IReadOnlyDictionary<string, TElement?> bDictionary)
56116
{
57-
if (!bDict.TryGetValue(key, out var bValue)
58-
|| !elementComparer.Equals(element, bValue))
117+
if (aDictionary.Count != bDictionary.Count)
59118
{
60119
return false;
61120
}
121+
122+
foreach (var pair in aDictionary)
123+
{
124+
if (!bDictionary.TryGetValue(pair.Key, out var bValue)
125+
|| !elementComparer.Equals(pair.Value, bValue))
126+
{
127+
return false;
128+
}
129+
}
130+
131+
return true;
62132
}
63133

64-
return true;
134+
throw new InvalidOperationException(
135+
CosmosStrings.BadDictionaryType(
136+
(a is IDictionary<string, TElement?> ? b : a).GetType().ShortDisplayName(),
137+
typeof(IDictionary<,>).MakeGenericType(typeof(string), elementComparer.Type).ShortDisplayName()));
65138
}
66139

67-
private static int GetHashCode(TCollection source, ValueComparer<TElement> elementComparer)
140+
private static int GetHashCode(IEnumerable source, ValueComparer elementComparer)
68141
{
142+
if (source is not IReadOnlyDictionary<string, TElement?> sourceDictionary)
143+
{
144+
throw new InvalidOperationException(
145+
CosmosStrings.BadDictionaryType(
146+
source.GetType().ShortDisplayName(),
147+
typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName()));
148+
}
149+
69150
var hash = new HashCode();
70-
foreach (var (key, element) in source)
151+
152+
foreach (var pair in sourceDictionary)
71153
{
72-
hash.Add(key);
73-
hash.Add(element, elementComparer);
154+
hash.Add(pair.Key);
155+
hash.Add(pair.Value == null ? 0 : elementComparer.GetHashCode(pair.Value));
74156
}
75157

76158
return hash.ToHashCode();
77159
}
78160

79-
private static TCollection Snapshot(TCollection source, ValueComparer<TElement> elementComparer, bool readOnly)
161+
private static IReadOnlyDictionary<string, TElement?> Snapshot(object source, ValueComparer elementComparer)
80162
{
81-
if (readOnly)
163+
if (source is not IReadOnlyDictionary<string, TElement?> sourceDictionary)
82164
{
83-
return source;
165+
throw new InvalidOperationException(
166+
CosmosStrings.BadDictionaryType(
167+
source.GetType().ShortDisplayName(),
168+
typeof(IDictionary<,>).MakeGenericType(typeof(string), elementComparer.Type).ShortDisplayName()));
84169
}
85170

86-
var snapshot = new Dictionary<string, TElement>(((IReadOnlyDictionary<string, TElement>)source).Count);
87-
foreach (var (key, element) in source)
171+
var snapshot = new Dictionary<string, TElement?>();
172+
foreach (var pair in sourceDictionary)
88173
{
89-
snapshot.Add(key, element is null ? default! : elementComparer.Snapshot(element));
174+
snapshot[pair.Key] = pair.Value == null ? default : (TElement?)elementComparer.Snapshot(pair.Value);
90175
}
91176

92-
return (TCollection)(object)snapshot;
177+
return snapshot;
93178
}
94179
}

Diff for: src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/EFCore.Cosmos/Properties/CosmosStrings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@
120120
<data name="AnalyticalTTLMismatch" xml:space="preserve">
121121
<value>The time to live for analytical store was configured to '{ttl1}' on '{entityType1}', but on '{entityType2}' it was configured to '{ttl2}'. All entity types mapped to the same container '{container}' must be configured with the same time to live for analytical store.</value>
122122
</data>
123+
<data name="BadDictionaryType" xml:space="preserve">
124+
<value>The type '{givenType}' cannot be mapped as a dictionary because it does not implement '{dictionaryType}'.</value>
125+
</data>
123126
<data name="CanConnectNotSupported" xml:space="preserve">
124127
<value>The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'.</value>
125128
</data>

Diff for: src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs

+77-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Text.Json;
46
using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
7+
using Microsoft.EntityFrameworkCore.Storage.Internal;
8+
using Microsoft.EntityFrameworkCore.Storage.Json;
59
using Newtonsoft.Json.Linq;
610

711
namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
@@ -103,11 +107,12 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
103107
return null;
104108
}
105109

106-
var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType);
107-
108110
if (clrType is { IsGenericType: true, IsGenericTypeDefinition: false })
109111
{
110112
var genericTypeDefinition = clrType.GetGenericTypeDefinition();
113+
114+
// This is legacy type mapping support for dictionaries in Cosmos. This needs to be consolidated with the relational
115+
// support, but for now this is being added back in to avoid a regression in EF9.
111116
if (genericTypeDefinition == typeof(Dictionary<,>)
112117
|| genericTypeDefinition == typeof(IDictionary<,>)
113118
|| genericTypeDefinition == typeof(IReadOnlyDictionary<,>))
@@ -122,11 +127,24 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
122127
var elementMappingInfo = new TypeMappingInfo(elementType);
123128
elementMapping = FindPrimitiveMapping(elementMappingInfo)
124129
?? FindCollectionMapping(elementMappingInfo);
125-
return elementMapping == null
126-
? null
127-
: new CosmosTypeMapping(
128-
clrType, CreateStringDictionaryComparer(elementMapping, elementType, clrType),
130+
131+
if (elementMapping != null)
132+
{
133+
var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType);
134+
if (jsonValueReaderWriter == null
135+
&& elementMapping.JsonValueReaderWriter != null)
136+
{
137+
jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
138+
typeof(PlaceholderJsonStringKeyedDictionaryReaderWriter<>)
139+
.MakeGenericType(elementMapping.JsonValueReaderWriter.ValueType),
140+
elementMapping.JsonValueReaderWriter);
141+
}
142+
143+
return new CosmosTypeMapping(
144+
clrType,
145+
CreateStringDictionaryComparer(elementMapping, elementType, clrType),
129146
jsonValueReaderWriter: jsonValueReaderWriter);
147+
}
130148
}
131149
}
132150

@@ -143,9 +161,59 @@ private static ValueComparer CreateStringDictionaryComparer(
143161

144162
return (ValueComparer)Activator.CreateInstance(
145163
elementType == unwrappedType
146-
? typeof(StringDictionaryComparer<,>).MakeGenericType(elementType, dictType)
164+
? typeof(StringDictionaryComparer<,>).MakeGenericType(dictType, elementType)
147165
: typeof(NullableStringDictionaryComparer<,>).MakeGenericType(unwrappedType, dictType),
148-
elementMapping.Comparer,
149-
readOnly)!;
166+
elementMapping.Comparer)!;
167+
}
168+
169+
// This ensures that the element reader/writers are not null when using Cosmos dictionary type mappings, but
170+
// is never actually used because Cosmos does not (yet) read and write JSON using this mechanism.
171+
/// <summary>
172+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
173+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
174+
/// any release. You should only use it directly in your code with extreme caution and knowing that
175+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
176+
/// </summary>
177+
#pragma warning disable EF1001
178+
public sealed class PlaceholderJsonStringKeyedDictionaryReaderWriter<TElement>(JsonValueReaderWriter elementReaderWriter)
179+
: JsonValueReaderWriter<IEnumerable<KeyValuePair<string, TElement>>>, ICompositeJsonValueReaderWriter
180+
#pragma warning restore EF1001
181+
{
182+
private readonly JsonValueReaderWriter<TElement> _elementReaderWriter = (JsonValueReaderWriter<TElement>)elementReaderWriter;
183+
184+
/// <summary>
185+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
186+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
187+
/// any release. You should only use it directly in your code with extreme caution and knowing that
188+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
189+
/// </summary>
190+
public override IEnumerable<KeyValuePair<string, TElement>> FromJsonTyped(
191+
ref Utf8JsonReaderManager manager,
192+
object? existingObject = null)
193+
=> throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
194+
195+
/// <summary>
196+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
197+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
198+
/// any release. You should only use it directly in your code with extreme caution and knowing that
199+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
200+
/// </summary>
201+
public override void ToJsonTyped(Utf8JsonWriter writer, IEnumerable<KeyValuePair<string, TElement>> value)
202+
=> throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
203+
204+
JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter
205+
=> _elementReaderWriter;
206+
207+
private readonly ConstructorInfo _constructorInfo
208+
= typeof(PlaceholderJsonStringKeyedDictionaryReaderWriter<TElement>)
209+
.GetConstructor([typeof(JsonValueReaderWriter<TElement>)])!;
210+
211+
/// <inheritdoc />
212+
public override Expression ConstructorExpression
213+
#pragma warning disable EF9100
214+
#pragma warning disable EF1001
215+
=> Expression.New(_constructorInfo, ((ICompositeJsonValueReaderWriter)this).InnerReaderWriter.ConstructorExpression);
216+
#pragma warning restore EF1001
217+
#pragma warning restore EF9100
150218
}
151219
}

0 commit comments

Comments
 (0)