diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs index a81fd414687..b793f7d655d 100644 --- a/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs +++ b/src/EFCore.Cosmos/ChangeTracking/Internal/NullableStringDictionaryComparer.cs @@ -19,11 +19,11 @@ public sealed class NullableStringDictionaryComparer : Va /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public NullableStringDictionaryComparer(ValueComparer elementComparer, bool readOnly) + public NullableStringDictionaryComparer(ValueComparer elementComparer) : base( (a, b) => Compare(a, b, (ValueComparer)elementComparer), o => GetHashCode(o, (ValueComparer)elementComparer), - source => Snapshot(source, (ValueComparer)elementComparer, readOnly)) + source => Snapshot(source, (ValueComparer)elementComparer)) { } @@ -92,13 +92,8 @@ private static int GetHashCode(TCollection source, ValueComparer eleme return hash.ToHashCode(); } - private static TCollection Snapshot(TCollection source, ValueComparer elementComparer, bool readOnly) + private static TCollection Snapshot(TCollection source, ValueComparer elementComparer) { - if (readOnly) - { - return source; - } - var snapshot = new Dictionary(((IReadOnlyDictionary)source).Count); foreach (var (key, element) in source) { diff --git a/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs b/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs index 00e7de11f15..21b8f7fbf53 100644 --- a/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs +++ b/src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal; /// @@ -9,21 +12,30 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public sealed class StringDictionaryComparer : ValueComparer - where TCollection : class, IEnumerable> +public sealed class StringDictionaryComparer : ValueComparer, IInfrastructure { + private static readonly MethodInfo CompareMethod = typeof(StringDictionaryComparer).GetMethod( + nameof(Compare), BindingFlags.Static | BindingFlags.NonPublic, [typeof(object), typeof(object), typeof(ValueComparer)])!; + + private static readonly MethodInfo GetHashCodeMethod = typeof(StringDictionaryComparer).GetMethod( + nameof(GetHashCode), BindingFlags.Static | BindingFlags.NonPublic, [typeof(IEnumerable), typeof(ValueComparer)])!; + + private static readonly MethodInfo SnapshotMethod = typeof(StringDictionaryComparer).GetMethod( + nameof(Snapshot), BindingFlags.Static | BindingFlags.NonPublic, [typeof(object), typeof(ValueComparer)])!; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public StringDictionaryComparer(ValueComparer elementComparer, bool readOnly) + public StringDictionaryComparer(ValueComparer elementComparer) : base( - (a, b) => Compare(a, b, (ValueComparer)elementComparer), - o => GetHashCode(o, (ValueComparer)elementComparer), - source => Snapshot(source, (ValueComparer)elementComparer, readOnly)) + CompareLambda(elementComparer), + GetHashCodeLambda(elementComparer), + SnapshotLambda(elementComparer)) { + ElementComparer = elementComparer; } /// @@ -32,63 +44,136 @@ public StringDictionaryComparer(ValueComparer elementComparer, bool readOnly) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override Type Type - => typeof(TCollection); + public ValueComparer ElementComparer { get; } + + ValueComparer IInfrastructure.Instance => ElementComparer; + + private static Expression> CompareLambda(ValueComparer elementComparer) + { + var prm1 = Expression.Parameter(typeof(object), "a"); + var prm2 = Expression.Parameter(typeof(object), "b"); + + return Expression.Lambda>( + Expression.Call( + CompareMethod, + prm1, + prm2, +#pragma warning disable EF9100 + elementComparer.ConstructorExpression), +#pragma warning restore EF9100 + prm1, + prm2); + } + + private static Expression> GetHashCodeLambda(ValueComparer elementComparer) + { + var prm = Expression.Parameter(typeof(object), "o"); + + return Expression.Lambda>( + Expression.Call( + GetHashCodeMethod, + Expression.Convert( + prm, + typeof(IEnumerable)), +#pragma warning disable EF9100 + elementComparer.ConstructorExpression), +#pragma warning restore EF9100 + prm); + } + + private static Expression> SnapshotLambda(ValueComparer elementComparer) + { + var prm = Expression.Parameter(typeof(object), "source"); - private static bool Compare(TCollection? a, TCollection? b, ValueComparer elementComparer) + return Expression.Lambda>( + Expression.Call( + SnapshotMethod, + prm, +#pragma warning disable EF9100 + elementComparer.ConstructorExpression), +#pragma warning restore EF9100 + prm); + } + + private static bool Compare(object? a, object? b, ValueComparer elementComparer) { - if (a is not IReadOnlyDictionary aDict) + if (ReferenceEquals(a, b)) { - return b is not IReadOnlyDictionary; + return true; } - if (b is not IReadOnlyDictionary bDict || aDict.Count != bDict.Count) + if (a is null) { - return false; + return b is null; } - if (ReferenceEquals(aDict, bDict)) + if (b is null) { - return true; + return false; } - foreach (var (key, element) in aDict) + if (a is IReadOnlyDictionary aDictionary && b is IReadOnlyDictionary bDictionary) { - if (!bDict.TryGetValue(key, out var bValue) - || !elementComparer.Equals(element, bValue)) + if (aDictionary.Count != bDictionary.Count) { return false; } + + foreach (var pair in aDictionary) + { + if (!bDictionary.TryGetValue(pair.Key, out var bValue) + || !elementComparer.Equals(pair.Value, bValue)) + { + return false; + } + } + + return true; } - return true; + throw new InvalidOperationException( + CosmosStrings.BadDictionaryType( + (a is IDictionary ? b : a).GetType().ShortDisplayName(), + typeof(IDictionary<,>).MakeGenericType(typeof(string), elementComparer.Type).ShortDisplayName())); } - private static int GetHashCode(TCollection source, ValueComparer elementComparer) + private static int GetHashCode(IEnumerable source, ValueComparer elementComparer) { + if (source is not IReadOnlyDictionary sourceDictionary) + { + throw new InvalidOperationException( + CosmosStrings.BadDictionaryType( + source.GetType().ShortDisplayName(), + typeof(IList<>).MakeGenericType(elementComparer.Type).ShortDisplayName())); + } + var hash = new HashCode(); - foreach (var (key, element) in source) + + foreach (var pair in sourceDictionary) { - hash.Add(key); - hash.Add(element, elementComparer); + hash.Add(pair.Key); + hash.Add(pair.Value == null ? 0 : elementComparer.GetHashCode(pair.Value)); } return hash.ToHashCode(); } - private static TCollection Snapshot(TCollection source, ValueComparer elementComparer, bool readOnly) + private static IReadOnlyDictionary Snapshot(object source, ValueComparer elementComparer) { - if (readOnly) + if (source is not IReadOnlyDictionary sourceDictionary) { - return source; + throw new InvalidOperationException( + CosmosStrings.BadDictionaryType( + source.GetType().ShortDisplayName(), + typeof(IDictionary<,>).MakeGenericType(typeof(string), elementComparer.Type).ShortDisplayName())); } - var snapshot = new Dictionary(((IReadOnlyDictionary)source).Count); - foreach (var (key, element) in source) + var snapshot = new Dictionary(); + foreach (var pair in sourceDictionary) { - snapshot.Add(key, element is null ? default! : elementComparer.Snapshot(element)); + snapshot[pair.Key] = pair.Value == null ? default : (TElement?)elementComparer.Snapshot(pair.Value); } - return (TCollection)(object)snapshot; + return snapshot; } } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index d9da4c59af6..ec0701e6416 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -31,6 +31,14 @@ public static string AnalyticalTTLMismatch(object? ttl1, object? entityType1, ob GetString("AnalyticalTTLMismatch", nameof(ttl1), nameof(entityType1), nameof(entityType2), nameof(ttl2), nameof(container)), ttl1, entityType1, entityType2, ttl2, container); + /// + /// The type '{givenType}' cannot be mapped as a dictionary because it does not implement '{dictionaryType}'. + /// + public static string BadDictionaryType(object? givenType, object? dictionaryType) + => string.Format( + GetString("BadDictionaryType", nameof(givenType), nameof(dictionaryType)), + givenType, dictionaryType); + /// /// The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index ae5e1752570..9767a0ba7f7 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -120,6 +120,9 @@ 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. + + The type '{givenType}' cannot be mapped as a dictionary because it does not implement '{dictionaryType}'. + The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs index 737deb51155..b4055de9e77 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; using Newtonsoft.Json.Linq; namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; @@ -103,11 +107,12 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) return null; } - var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType); - if (clrType is { IsGenericType: true, IsGenericTypeDefinition: false }) { var genericTypeDefinition = clrType.GetGenericTypeDefinition(); + + // This is legacy type mapping support for dictionaries in Cosmos. This needs to be consolidated with the relational + // support, but for now this is being added back in to avoid a regression in EF9. if (genericTypeDefinition == typeof(Dictionary<,>) || genericTypeDefinition == typeof(IDictionary<,>) || genericTypeDefinition == typeof(IReadOnlyDictionary<,>)) @@ -122,11 +127,24 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) var elementMappingInfo = new TypeMappingInfo(elementType); elementMapping = FindPrimitiveMapping(elementMappingInfo) ?? FindCollectionMapping(elementMappingInfo); - return elementMapping == null - ? null - : new CosmosTypeMapping( - clrType, CreateStringDictionaryComparer(elementMapping, elementType, clrType), + + if (elementMapping != null) + { + var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType); + if (jsonValueReaderWriter == null + && elementMapping.JsonValueReaderWriter != null) + { + jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance( + typeof(PlaceholderJsonStringKeyedDictionaryReaderWriter<>) + .MakeGenericType(elementMapping.JsonValueReaderWriter.ValueType), + elementMapping.JsonValueReaderWriter); + } + + return new CosmosTypeMapping( + clrType, + CreateStringDictionaryComparer(elementMapping, elementType, clrType), jsonValueReaderWriter: jsonValueReaderWriter); + } } } @@ -143,9 +161,59 @@ private static ValueComparer CreateStringDictionaryComparer( return (ValueComparer)Activator.CreateInstance( elementType == unwrappedType - ? typeof(StringDictionaryComparer<,>).MakeGenericType(elementType, dictType) + ? typeof(StringDictionaryComparer<,>).MakeGenericType(dictType, elementType) : typeof(NullableStringDictionaryComparer<,>).MakeGenericType(unwrappedType, dictType), - elementMapping.Comparer, - readOnly)!; + elementMapping.Comparer)!; + } + + // This ensures that the element reader/writers are not null when using Cosmos dictionary type mappings, but + // is never actually used because Cosmos does not (yet) read and write JSON using this mechanism. + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// +#pragma warning disable EF1001 + public sealed class PlaceholderJsonStringKeyedDictionaryReaderWriter(JsonValueReaderWriter elementReaderWriter) + : JsonValueReaderWriter>>, ICompositeJsonValueReaderWriter +#pragma warning restore EF1001 + { + private readonly JsonValueReaderWriter _elementReaderWriter = (JsonValueReaderWriter)elementReaderWriter; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override IEnumerable> FromJsonTyped( + ref Utf8JsonReaderManager manager, + object? existingObject = null) + => throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos."); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override void ToJsonTyped(Utf8JsonWriter writer, IEnumerable> value) + => throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos."); + + JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter + => _elementReaderWriter; + + private readonly ConstructorInfo _constructorInfo + = typeof(PlaceholderJsonStringKeyedDictionaryReaderWriter) + .GetConstructor([typeof(JsonValueReaderWriter)])!; + + /// + public override Expression ConstructorExpression +#pragma warning disable EF9100 +#pragma warning disable EF1001 + => Expression.New(_constructorInfo, ((ICompositeJsonValueReaderWriter)this).InnerReaderWriter.ConstructorExpression); +#pragma warning restore EF1001 +#pragma warning restore EF9100 } } diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index 001fdb41e8a..1c9e62e5b76 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -879,15 +879,13 @@ await Can_add_update_delete_with_collection( }, new List { new byte?[] { 3, null }, null }); - // TODO: Dictionary mapping Issue #29825 - // await Can_add_update_delete_with_collection>>( - // new Dictionary[] { new() { { "1", null } } }, - // c => - // { - // var dictionary = c.Collection[0]["3"] = "2"; - // }, - // new List> { new() { { "1", null }, { "3", "2" } } }, - // onModelBuilder: b => b.Entity>>>().PrimitiveCollection(e => e.Collection)); + await Can_add_update_delete_with_collection>>( + new Dictionary[] { new() { { "1", null } } }, + c => + { + var dictionary = c.Collection[0]["3"] = "2"; + }, + new List> { new() { { "1", null }, { "3", "2" } } }); await Can_add_update_delete_with_collection( [[1f], [2]], @@ -905,24 +903,49 @@ await Can_add_update_delete_with_collection( }, new[] { new decimal?[] { 1, 3 } }); - // TODO: Dictionary mapping Issue #29825 - // await Can_add_update_delete_with_collection( - // new Dictionary> { { "1", [1] } }, - // c => - // { - // c.Collection["2"] = [3]; - // }, - // new Dictionary> { { "1", [1] }, { "2", [3] } }); - - // TODO: Dictionary mapping Issue #29825 - // await Can_add_update_delete_with_collection>( - // new SortedDictionary { { "2", [2] }, { "1", [1] } }, - // c => - // { - // c.Collection.Clear(); - // c.Collection["2"] = null; - // }, - // new SortedDictionary { { "2", null } }); + await Can_add_update_delete_with_collection( + new Dictionary> { { "1", [1] } }, + c => + { + c.Collection["2"] = [3]; + }, + new Dictionary> { { "1", [1] }, { "2", [3] } }); + + // Issue #34105 + await Can_add_update_delete_with_collection( + new Dictionary { { "1", ["1"] } }, + c => + { + c.Collection["2"] = ["3"]; + }, + new Dictionary { { "1", ["1"] }, { "2", ["3"] } }); + + await Can_add_update_delete_with_collection>( + new SortedDictionary { { "2", [2] }, { "1", [1] } }, + c => + { + c.Collection.Clear(); + c.Collection["2"] = null; + }, + new SortedDictionary { { "2", null } }); + + await Can_add_update_delete_with_collection>>( + new Dictionary> + { + { "2" , new Dictionary { { "value", 2 } } }, + { "1" , new Dictionary { { "value", 1 } } } + }, + c => + { + c.Collection = new Dictionary> + { + { "1", new Dictionary { { "value", 1 } } }, { "2", null } + }; + }, + new Dictionary> + { + { "1", new Dictionary { { "value", 1 } } }, { "2", null } + }); await Can_add_update_delete_with_collection>>( ImmutableDictionary>.Empty diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs index 3231aa88c7e..ecd47a73ff1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs @@ -7,6 +7,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -32,7 +33,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+Data", typeof(CompiledModelTestBase.Data), baseEntityType, - propertyCount: 6, + propertyCount: 8, keyCount: 2); var id = runtimeEntityType.AddProperty( @@ -161,21 +162,105 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas byte[] (string v) => Convert.FromBase64String(v)))); blob.AddAnnotation("Cosmos:PropertyName", "JsonBlob"); + var list = runtimeEntityType.AddProperty( + "List", + typeof(List>), + nullable: true); + list.SetAccessors( + List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(2), + List> (InternalEntityEntry entry) => entry.ReadShadowValue>>(2), + List> (InternalEntityEntry entry) => entry.ReadOriginalValue>>(list, 3), + List> (InternalEntityEntry entry) => entry.GetCurrentValue>>(list), + object (ValueBuffer valueBuffer) => valueBuffer[3]); + list.SetPropertyIndexes( + index: 3, + originalValueIndex: 3, + shadowIndex: 2, + relationshipIndex: -1, + storeGenerationIndex: -1); + list.TypeMapping = CosmosTypeMapping.Default.Clone( + comparer: new ListOfReferenceTypesComparer>, Dictionary>(new StringDictionaryComparer, int>(new ValueComparer( + bool (int v1, int v2) => v1 == v2, + int (int v) => v, + int (int v) => v))), + keyComparer: new ValueComparer>>( + bool (List> v1, List> v2) => object.Equals(v1, v2), + int (List> v) => ((object)v).GetHashCode(), + List> (List> v) => v), + providerValueComparer: new ValueComparer>>( + bool (List> v1, List> v2) => object.Equals(v1, v2), + int (List> v) => ((object)v).GetHashCode(), + List> (List> v) => v), + clrType: typeof(List>), + jsonValueReaderWriter: new JsonCollectionOfReferencesReaderWriter>, Dictionary>( + new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter( + JsonInt32ReaderWriter.Instance)), + elementMapping: CosmosTypeMapping.Default.Clone( + comparer: new StringDictionaryComparer, int>(new ValueComparer( + bool (int v1, int v2) => v1 == v2, + int (int v) => v, + int (int v) => v)), + keyComparer: new ValueComparer>( + bool (Dictionary v1, Dictionary v2) => object.Equals(v1, v2), + int (Dictionary v) => ((object)v).GetHashCode(), + Dictionary (Dictionary v) => v), + providerValueComparer: new ValueComparer>( + bool (Dictionary v1, Dictionary v2) => object.Equals(v1, v2), + int (Dictionary v) => ((object)v).GetHashCode(), + Dictionary (Dictionary v) => v), + clrType: typeof(Dictionary), + jsonValueReaderWriter: new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter( + JsonInt32ReaderWriter.Instance))); + + var map = runtimeEntityType.AddProperty( + "Map", + typeof(Dictionary), + nullable: true); + map.SetAccessors( + Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(3), + Dictionary (InternalEntityEntry entry) => entry.ReadShadowValue>(3), + Dictionary (InternalEntityEntry entry) => entry.ReadOriginalValue>(map, 4), + Dictionary (InternalEntityEntry entry) => entry.GetCurrentValue>(map), + object (ValueBuffer valueBuffer) => valueBuffer[4]); + map.SetPropertyIndexes( + index: 4, + originalValueIndex: 4, + shadowIndex: 3, + relationshipIndex: -1, + storeGenerationIndex: -1); + map.TypeMapping = CosmosTypeMapping.Default.Clone( + comparer: new StringDictionaryComparer, string[]>(new ListOfReferenceTypesComparer(new ValueComparer( + bool (string v1, string v2) => v1 == v2, + int (string v) => ((object)v).GetHashCode(), + string (string v) => v))), + keyComparer: new ValueComparer>( + bool (Dictionary v1, Dictionary v2) => object.Equals(v1, v2), + int (Dictionary v) => ((object)v).GetHashCode(), + Dictionary (Dictionary v) => v), + providerValueComparer: new ValueComparer>( + bool (Dictionary v1, Dictionary v2) => object.Equals(v1, v2), + int (Dictionary v) => ((object)v).GetHashCode(), + Dictionary (Dictionary v) => v), + clrType: typeof(Dictionary), + jsonValueReaderWriter: new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter( + new JsonCollectionOfReferencesReaderWriter( + JsonStringReaderWriter.Instance))); + var __id = runtimeEntityType.AddProperty( "__id", typeof(string), afterSaveBehavior: PropertySaveBehavior.Throw, valueGeneratorFactory: new IdValueGeneratorFactory().Create); __id.SetAccessors( - string (InternalEntityEntry entry) => entry.ReadShadowValue(2), - string (InternalEntityEntry entry) => entry.ReadShadowValue(2), - string (InternalEntityEntry entry) => entry.ReadOriginalValue(__id, 3), + string (InternalEntityEntry entry) => entry.ReadShadowValue(4), + string (InternalEntityEntry entry) => entry.ReadShadowValue(4), + string (InternalEntityEntry entry) => entry.ReadOriginalValue(__id, 5), string (InternalEntityEntry entry) => entry.ReadRelationshipSnapshotValue(__id, 2), - object (ValueBuffer valueBuffer) => valueBuffer[3]); + object (ValueBuffer valueBuffer) => valueBuffer[5]); __id.SetPropertyIndexes( - index: 3, - originalValueIndex: 3, - shadowIndex: 2, + index: 5, + originalValueIndex: 5, + shadowIndex: 4, relationshipIndex: 2, storeGenerationIndex: -1); __id.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -204,15 +289,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas beforeSaveBehavior: PropertySaveBehavior.Ignore, afterSaveBehavior: PropertySaveBehavior.Ignore); __jObject.SetAccessors( - JObject (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(4) ? entry.ReadStoreGeneratedValue(0) : (entry.FlaggedAsTemporary(4) && entry.ReadShadowValue(3) == null ? entry.ReadTemporaryValue(0) : entry.ReadShadowValue(3))), - JObject (InternalEntityEntry entry) => entry.ReadShadowValue(3), - JObject (InternalEntityEntry entry) => entry.ReadOriginalValue(__jObject, 4), + JObject (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(6) ? entry.ReadStoreGeneratedValue(0) : (entry.FlaggedAsTemporary(6) && entry.ReadShadowValue(5) == null ? entry.ReadTemporaryValue(0) : entry.ReadShadowValue(5))), + JObject (InternalEntityEntry entry) => entry.ReadShadowValue(5), + JObject (InternalEntityEntry entry) => entry.ReadOriginalValue(__jObject, 6), JObject (InternalEntityEntry entry) => entry.GetCurrentValue(__jObject), - object (ValueBuffer valueBuffer) => valueBuffer[4]); + object (ValueBuffer valueBuffer) => valueBuffer[6]); __jObject.SetPropertyIndexes( - index: 4, - originalValueIndex: 4, - shadowIndex: 3, + index: 6, + originalValueIndex: 6, + shadowIndex: 5, relationshipIndex: -1, storeGenerationIndex: 0); __jObject.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -240,15 +325,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas beforeSaveBehavior: PropertySaveBehavior.Ignore, afterSaveBehavior: PropertySaveBehavior.Ignore); _etag.SetAccessors( - string (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(5) ? entry.ReadStoreGeneratedValue(1) : (entry.FlaggedAsTemporary(5) && entry.ReadShadowValue(4) == null ? entry.ReadTemporaryValue(1) : entry.ReadShadowValue(4))), - string (InternalEntityEntry entry) => entry.ReadShadowValue(4), - string (InternalEntityEntry entry) => entry.ReadOriginalValue(_etag, 5), + string (InternalEntityEntry entry) => (entry.FlaggedAsStoreGenerated(7) ? entry.ReadStoreGeneratedValue(1) : (entry.FlaggedAsTemporary(7) && entry.ReadShadowValue(6) == null ? entry.ReadTemporaryValue(1) : entry.ReadShadowValue(6))), + string (InternalEntityEntry entry) => entry.ReadShadowValue(6), + string (InternalEntityEntry entry) => entry.ReadOriginalValue(_etag, 7), string (InternalEntityEntry entry) => entry.GetCurrentValue(_etag), - object (ValueBuffer valueBuffer) => valueBuffer[5]); + object (ValueBuffer valueBuffer) => valueBuffer[7]); _etag.SetPropertyIndexes( - index: 5, - originalValueIndex: 5, - shadowIndex: 4, + index: 7, + originalValueIndex: 7, + shadowIndex: 6, relationshipIndex: -1, storeGenerationIndex: 1); _etag.TypeMapping = CosmosTypeMapping.Default.Clone( @@ -282,6 +367,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var id = runtimeEntityType.FindProperty("Id")!; var partitionId = runtimeEntityType.FindProperty("PartitionId")!; var blob = runtimeEntityType.FindProperty("Blob")!; + var list = runtimeEntityType.FindProperty("List")!; + var map = runtimeEntityType.FindProperty("Map")!; var __id = runtimeEntityType.FindProperty("__id")!; var __jObject = runtimeEntityType.FindProperty("__jObject")!; var _etag = runtimeEntityType.FindProperty("_etag")!; @@ -295,16 +382,16 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) ISnapshot (InternalEntityEntry source) => { var entity = ((CompiledModelTestBase.Data)(source.Entity)); - return ((ISnapshot)(new Snapshot(((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetValueComparer())).Snapshot(source.GetCurrentValue(partitionId))), (source.GetCurrentValue(blob) == null ? null : ((ValueComparer)(((IProperty)blob).GetValueComparer())).Snapshot(source.GetCurrentValue(blob))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetValueComparer())).Snapshot(source.GetCurrentValue(__id))), (source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(source.GetCurrentValue(__jObject))), (source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(source.GetCurrentValue(_etag)))))); + return ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>(((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetValueComparer())).Snapshot(source.GetCurrentValue(partitionId))), (source.GetCurrentValue(blob) == null ? null : ((ValueComparer)(((IProperty)blob).GetValueComparer())).Snapshot(source.GetCurrentValue(blob))), (((object)(source.GetCurrentValue>>(list))) == null ? null : ((List>)(((ValueComparer)(((IProperty)list).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>>(list))))))), (((object)(source.GetCurrentValue>(map))) == null ? null : ((Dictionary)(((ValueComparer)(((IProperty)map).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(map))))))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetValueComparer())).Snapshot(source.GetCurrentValue(__id))), (source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(source.GetCurrentValue(__jObject))), (source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(source.GetCurrentValue(_etag)))))); }); runtimeEntityType.SetStoreGeneratedValuesFactory( ISnapshot () => ((ISnapshot)(new Snapshot((default(JObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(default(JObject))), (default(string) == null ? null : ((ValueComparer)(((IProperty)_etag).GetValueComparer())).Snapshot(default(string))))))); runtimeEntityType.SetTemporaryValuesFactory( ISnapshot (InternalEntityEntry source) => ((ISnapshot)(new Snapshot(default(JObject), default(string))))); runtimeEntityType.SetShadowValuesFactory( - ISnapshot (IDictionary source) => ((ISnapshot)(new Snapshot((source.ContainsKey("Id") ? ((int)(source["Id"])) : 0), (source.ContainsKey("PartitionId") ? ((long? )(source["PartitionId"])) : null), (source.ContainsKey("__id") ? ((string)(source["__id"])) : null), (source.ContainsKey("__jObject") ? ((JObject)(source["__jObject"])) : null), (source.ContainsKey("_etag") ? ((string)(source["_etag"])) : null))))); + ISnapshot (IDictionary source) => ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>((source.ContainsKey("Id") ? ((int)(source["Id"])) : 0), (source.ContainsKey("PartitionId") ? ((long? )(source["PartitionId"])) : null), (source.ContainsKey("List") ? ((List>)(source["List"])) : null), (source.ContainsKey("Map") ? ((Dictionary)(source["Map"])) : null), (source.ContainsKey("__id") ? ((string)(source["__id"])) : null), (source.ContainsKey("__jObject") ? ((JObject)(source["__jObject"])) : null), (source.ContainsKey("_etag") ? ((string)(source["_etag"])) : null))))); runtimeEntityType.SetEmptyShadowValuesFactory( - ISnapshot () => ((ISnapshot)(new Snapshot(default(int), default(long? ), default(string), default(JObject), default(string))))); + ISnapshot () => ((ISnapshot)(new Snapshot>, Dictionary, string, JObject, string>(default(int), default(long? ), default(List>), default(Dictionary), default(string), default(JObject), default(string))))); runtimeEntityType.SetRelationshipSnapshotFactory( ISnapshot (InternalEntityEntry source) => { @@ -312,11 +399,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) return ((ISnapshot)(new Snapshot(((ValueComparer)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(id)), (source.GetCurrentValue(partitionId) == null ? null : ((ValueComparer)(((IProperty)partitionId).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(partitionId))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(__id)))))); }); runtimeEntityType.Counts = new PropertyCounts( - propertyCount: 6, + propertyCount: 8, navigationCount: 0, complexPropertyCount: 0, - originalValueCount: 6, - shadowCount: 5, + originalValueCount: 8, + shadowCount: 7, relationshipCount: 3, storeGeneratedCount: 2); runtimeEntityType.AddAnnotation("Cosmos:ContainerName", "DataContainer"); diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs index 8055bee432b..f3fa917fdf4 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs @@ -29,6 +29,8 @@ public virtual Task Basic_cosmos_model() eb.HasPartitionKey("PartitionId"); eb.HasKey("Id", "PartitionId"); eb.ToContainer("DataContainer"); + eb.Property>("Map"); + eb.Property>>("List"); eb.UseETagConcurrency(); eb.HasNoDiscriminator(); eb.Property(d => d.Blob).ToJsonProperty("JsonBlob"); @@ -94,6 +96,38 @@ public virtual Task Basic_cosmos_model() Assert.NotNull(partitionId.GetValueComparer()); Assert.NotNull(partitionId.GetKeyValueComparer()); + var map = dataEntity.FindProperty("Map")!; + Assert.Equal(typeof(Dictionary), map.ClrType); + Assert.Null(map.PropertyInfo); + Assert.Null(map.FieldInfo); + Assert.True(map.IsNullable); + Assert.False(map.IsConcurrencyToken); + Assert.False(map.IsPrimitiveCollection); + Assert.Equal(ValueGenerated.Never, map.ValueGenerated); + Assert.Equal(PropertySaveBehavior.Save, map.GetAfterSaveBehavior()); + Assert.Equal(PropertySaveBehavior.Save, map.GetBeforeSaveBehavior()); + Assert.Equal("Map", CosmosPropertyExtensions.GetJsonPropertyName(map)); + Assert.Null(map.GetValueGeneratorFactory()); + Assert.Null(map.GetValueConverter()); + Assert.NotNull(map.GetValueComparer()); + Assert.NotNull(map.GetKeyValueComparer()); + + var list = dataEntity.FindProperty("List")!; + Assert.Equal(typeof(List>), list.ClrType); + Assert.Null(list.PropertyInfo); + Assert.Null(list.FieldInfo); + Assert.True(list.IsNullable); + Assert.False(list.IsConcurrencyToken); + Assert.False(list.IsPrimitiveCollection); + Assert.Equal(ValueGenerated.Never, list.ValueGenerated); + Assert.Equal(PropertySaveBehavior.Save, list.GetAfterSaveBehavior()); + Assert.Equal(PropertySaveBehavior.Save, list.GetBeforeSaveBehavior()); + Assert.Equal("List", CosmosPropertyExtensions.GetJsonPropertyName(list)); + Assert.Null(list.GetValueGeneratorFactory()); + Assert.Null(list.GetValueConverter()); + Assert.NotNull(list.GetValueComparer()); + Assert.NotNull(list.GetKeyValueComparer()); + var eTag = dataEntity.FindProperty("_etag")!; Assert.Equal(typeof(string), eTag.ClrType); Assert.Null(eTag.PropertyInfo); @@ -143,7 +177,7 @@ public virtual Task Basic_cosmos_model() Assert.Equal(2, dataEntity.GetKeys().Count()); - Assert.Equal([id, partitionId, blob, storeId, jObject, eTag], dataEntity.GetProperties()); + Assert.Equal([id, partitionId, blob, list, map, storeId, jObject, eTag], dataEntity.GetProperties()); }); protected override void BuildBigModel(ModelBuilder modelBuilder, bool jsonColumns)