From bef7f3eb6d79461df1770852f87137dc203d7b6a Mon Sep 17 00:00:00 2001 From: ajcvickers Date: Mon, 31 Aug 2020 08:21:07 -0700 Subject: [PATCH] Updates to shared-type entity type handling in proxies Fixes #22337 * Check for virtual indexer properties and throw unless type is Dictionary and has only primary keys * Specific exception for the Dictionary case * Verified many-to-many with payload requires this pattern and works See also #22336 --- .../Properties/ProxiesStrings.Designer.cs | 20 ++++- .../Properties/ProxiesStrings.resx | 8 +- .../Proxies/Internal/ProxyBindingRewriter.cs | 77 ++++++++++++++----- .../Proxies/Internal/ProxyFactory.cs | 5 ++ src/EFCore/Metadata/Internal/TypeBase.cs | 6 +- src/EFCore/Properties/CoreStrings.Designer.cs | 8 ++ src/EFCore/Properties/CoreStrings.resx | 3 + src/Shared/MethodInfoExtensions.cs | 6 ++ .../ChangeDetectionProxyTests.cs | 47 +++++++++++ test/EFCore.Proxies.Tests/ProxyTests.cs | 35 +++++++++ .../ManyToManyTrackingTestBase.cs | 27 ++++--- .../Query/ManyToManyQueryFixtureBase.cs | 4 +- .../TestModels/ManyToManyModel/EntityOne.cs | 4 +- .../TestModels/ManyToManyModel/EntityThree.cs | 4 +- .../ManyToManyModel/ManyToManyData.cs | 11 ++- .../ManyToManyModel/ProxyablePropertyBag.cs | 64 +++++++++++++++ .../ManyToManyLoadSqlServerTest.cs | 2 +- .../ManyToManyTrackingSqlServerTestBase.cs | 2 +- .../ManyToManyLoadSqliteTestBase.cs | 2 +- .../ManyToManyTrackingSqliteTest.cs | 2 +- 20 files changed, 286 insertions(+), 51 deletions(-) create mode 100644 test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ProxyablePropertyBag.cs diff --git a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs index 2cfedf6eafc..f970507e362 100644 --- a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs +++ b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs @@ -18,6 +18,14 @@ public static class ProxiesStrings private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.ProxiesStrings", typeof(ProxiesStrings).Assembly); + /// + /// '{dictionaryType}' is not suitable for use as a change-tracking proxy because its indexer property is not virtual. Consider using an implementation of '{interfaceType}' that allows overriding of the indexer. + /// + public static string DictionaryCannotBeProxied([CanBeNull] object dictionaryType, [CanBeNull] object interfaceType) + => string.Format( + GetString("DictionaryCannotBeProxied", nameof(dictionaryType), nameof(interfaceType)), + dictionaryType, interfaceType); + /// /// Property '{property}' on entity type '{entityType}' is mapped without a CLR property. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. /// @@ -35,11 +43,19 @@ public static string ItsASeal([CanBeNull] object entityType) entityType); /// - /// Property '{property}' on entity type '{entityType}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. + /// The mapped indexer property on entity type '{entityType}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. + /// + public static string NonVirtualIndexerProperty([CanBeNull] object entityType) + => string.Format( + GetString("NonVirtualIndexerProperty", nameof(entityType)), + entityType); + + /// + /// Property '{1_entityType}.{0_property}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. /// public static string NonVirtualProperty([CanBeNull] object property, [CanBeNull] object entityType) => string.Format( - GetString("NonVirtualProperty", nameof(property), nameof(entityType)), + GetString("NonVirtualProperty", "0_property", "1_entityType"), property, entityType); /// diff --git a/src/EFCore.Proxies/Properties/ProxiesStrings.resx b/src/EFCore.Proxies/Properties/ProxiesStrings.resx index 703375d6814..b5c25a09c9d 100644 --- a/src/EFCore.Proxies/Properties/ProxiesStrings.resx +++ b/src/EFCore.Proxies/Properties/ProxiesStrings.resx @@ -117,14 +117,20 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + '{dictionaryType}' is not suitable for use as a change-tracking proxy because its indexer property is not virtual. Consider using an implementation of '{interfaceType}' that allows overriding of the indexer. + Property '{property}' on entity type '{entityType}' is mapped without a CLR property. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. Entity type '{entityType}' is sealed. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. + + The mapped indexer property on entity type '{entityType}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. + - Property '{property}' on entity type '{entityType}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. + Property '{1_entityType}.{0_property}' is not virtual. UseChangeTrackingProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual. Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseChangeTrackingProxies' or 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable proxies. diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs index 6f107c4a8b4..e3503ec5ec1 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs @@ -65,9 +65,10 @@ public virtual void ProcessModelFinalizing( { foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) { - if (entityType.ClrType?.IsAbstract == false) + var clrType = entityType.ClrType; + if (clrType?.IsAbstract == false) { - if (entityType.ClrType.IsSealed) + if (clrType.IsSealed) { throw new InvalidOperationException(ProxiesStrings.ItsASeal(entityType.DisplayName())); } @@ -102,7 +103,7 @@ public virtual void ProcessModelFinalizing( } if (_options.UseChangeTrackingProxies - && navigationBase.PropertyInfo.SetMethod?.IsVirtual == false) + && navigationBase.PropertyInfo.SetMethod?.IsReallyVirtual() == false) { throw new InvalidOperationException( ProxiesStrings.NonVirtualProperty(navigationBase.Name, entityType.DisplayName())); @@ -110,7 +111,7 @@ public virtual void ProcessModelFinalizing( if (_options.UseLazyLoadingProxies) { - if (!navigationBase.PropertyInfo.GetMethod.IsVirtual + if (!navigationBase.PropertyInfo.GetMethod.IsReallyVirtual() && (!(navigationBase is INavigation navigation && navigation.ForeignKey.IsOwnership))) { @@ -122,6 +123,59 @@ public virtual void ProcessModelFinalizing( } } + if (_options.UseChangeTrackingProxies) + { + var indexerChecked = false; + foreach (var property in entityType.GetDeclaredProperties() + .Where(p => !p.IsShadowProperty())) + { + if (property.IsIndexerProperty()) + { + if (!indexerChecked) + { + indexerChecked = true; + + if (!property.PropertyInfo.SetMethod.IsReallyVirtual()) + { + if (clrType.IsGenericType + && clrType.GetGenericTypeDefinition() == typeof(Dictionary<,>) + && clrType.GenericTypeArguments[0] == typeof(string)) + { + if (entityType.GetProperties().Any(p => !p.IsPrimaryKey())) + { + throw new InvalidOperationException( + ProxiesStrings.DictionaryCannotBeProxied( + clrType.ShortDisplayName(), + typeof(IDictionary<,>).MakeGenericType(clrType.GenericTypeArguments) + .ShortDisplayName())); + } + } + else + { + throw new InvalidOperationException( + ProxiesStrings.NonVirtualIndexerProperty(entityType.DisplayName())); + } + } + } + } + else + { + if (property.PropertyInfo == null) + { + throw new InvalidOperationException( + ProxiesStrings.FieldProperty(property.Name, entityType.DisplayName())); + } + + if (property.PropertyInfo.SetMethod?.IsReallyVirtual() == false) + { + throw new InvalidOperationException( + ProxiesStrings.NonVirtualProperty(property.Name, entityType.DisplayName())); + } + } + + } + } + void UpdateConstructorBindings(string bindingAnnotationName, InstantiationBinding binding) { if (_options.UseLazyLoadingProxies) @@ -176,21 +230,6 @@ void UpdateConstructorBindings(string bindingAnnotationName, InstantiationBindin new ObjectArrayParameterBinding(binding.ParameterBindings) }, proxyType)); - - foreach (var prop in entityType.GetDeclaredProperties().Where(p => !p.IsShadowProperty())) - { - if (prop.PropertyInfo == null) - { - throw new InvalidOperationException( - ProxiesStrings.FieldProperty(prop.Name, entityType.DisplayName())); - } - - if (prop.PropertyInfo.SetMethod?.IsVirtual == false) - { - throw new InvalidOperationException( - ProxiesStrings.NonVirtualProperty(prop.Name, entityType.DisplayName())); - } - } } } } diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs index 94dfc9a58d0..587fc3386a9 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs @@ -41,6 +41,11 @@ public virtual object Create( var entityType = context.Model.FindRuntimeEntityType(type); if (entityType == null) { + if (context.Model.IsShared(type)) + { + throw new InvalidOperationException(CoreStrings.EntityTypeNotFoundSharedProxy(type.ShortDisplayName())); + } + throw new InvalidOperationException(CoreStrings.EntityTypeNotFound(type.ShortDisplayName())); } diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index b30aa208ce7..d6cc6fcd6e6 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -60,8 +60,10 @@ protected TypeBase([NotNull] Type type, [NotNull] Model model, ConfigurationSour Name = model.GetDisplayName(type); ClrType = type; - HasSharedClrType = false; - IsPropertyBag = type.IsPropertyBagType(); + + var isPropertyBag = type.IsPropertyBagType(); + IsPropertyBag = isPropertyBag; + HasSharedClrType = isPropertyBag; } /// diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 7c866abd2e3..8dcf1bf242c 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -770,6 +770,14 @@ public static string EntityTypeNotFound([CanBeNull] object entityType) GetString("EntityTypeNotFound", nameof(entityType)), entityType); + /// + /// The type '{clrType}' is configured as a shared-type entity type, but the entity type name is not known. Ensure that CreateProxy is called on a DbSet created specifically for the shared-type entity type through use of a `DbContext.Set` overload that accepts an entity type name. + /// + public static string EntityTypeNotFoundSharedProxy([CanBeNull] object clrType) + => string.Format( + GetString("EntityTypeNotFoundSharedProxy", nameof(clrType)), + clrType); + /// /// The specified entity type '{entityType}' is invalid. It should be either the dependent entity type '{dependentType}' or the principal entity type '{principalType}' or an entity type derived from one of them. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 576f6b0b83b..18fd529d54f 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -402,6 +402,9 @@ The entity type '{entityType}' was not found. Ensure that the entity type has been added to the model. + + The type '{clrType}' is configured as a shared-type entity type, but the entity type name is not known. Ensure that CreateProxy is called on a DbSet created specifically for the shared-type entity type through use of a `DbContext.Set` overload that accepts an entity type name. + The specified entity type '{entityType}' is invalid. It should be either the dependent entity type '{dependentType}' or the principal entity type '{principalType}' or an entity type derived from one of them. diff --git a/src/Shared/MethodInfoExtensions.cs b/src/Shared/MethodInfoExtensions.cs index c60728cf13a..5a785df801d 100644 --- a/src/Shared/MethodInfoExtensions.cs +++ b/src/Shared/MethodInfoExtensions.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace System.Reflection { @@ -14,5 +15,10 @@ public static bool IsContainsMethod(this MethodInfo method) && method.DeclaringType.GetInterfaces().Append(method.DeclaringType).Any( t => t == typeof(IList) || (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ICollection<>))); + + + public static bool IsReallyVirtual([NotNull] this MethodInfo method) + => method.IsVirtual && !method.IsFinal; + } } diff --git a/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs index 26fdf97aec8..2291a4fb277 100644 --- a/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs +++ b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Reflection.Metadata; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -33,6 +36,24 @@ public void Throws_if_non_virtual_property() () => context.Model).Message); } + [ConditionalFact] + public void Throws_if_non_virtual_indexer_property() + { + using var context = new ChangeContext(entityBuilderAction: b => b.IndexerProperty("Snoopy")); + Assert.Equal( + ProxiesStrings.NonVirtualIndexerProperty(nameof(ChangeNonVirtualIndexer)), + Assert.Throws(() => context.Model).Message); + } + + [ConditionalFact] + public void Does_not_throw_when_non_virtual_indexer_not_mapped() + { + using var context = new ChangeContext(); + + Assert.False( + context.Model.FindEntityType(typeof(ChangeNonVirtualIndexerNotUsed)).GetProperties().Any(e => e.IsIndexerProperty())); + } + [ConditionalFact] public void Throws_if_non_virtual_navigation() { @@ -318,6 +339,32 @@ public class ChangeNonVirtualNavEntity public ChangeNonVirtualNavEntity SelfRef { get; set; } } + public class ChangeNonVirtualIndexer + { + private readonly Dictionary _keyValuePairs = new Dictionary(); + + public virtual int Id { get; set; } + + public object this[string key] + { + get => _keyValuePairs[key]; + set => _keyValuePairs[key] = value; + } + } + + public class ChangeNonVirtualIndexerNotUsed + { + private readonly Dictionary _keyValuePairs = new Dictionary(); + + public virtual int Id { get; set; } + + public object this[string key] + { + get => _keyValuePairs[key]; + set => _keyValuePairs[key] = value; + } + } + public class ChangeValueEntity { public virtual int Id { get; set; } diff --git a/test/EFCore.Proxies.Tests/ProxyTests.cs b/test/EFCore.Proxies.Tests/ProxyTests.cs index e98c741e413..fa5ab6a4ad9 100644 --- a/test/EFCore.Proxies.Tests/ProxyTests.cs +++ b/test/EFCore.Proxies.Tests/ProxyTests.cs @@ -71,6 +71,33 @@ public void Materialization_uses_parameterized_constructor_taking_context() } } + [ConditionalFact] + public void CreateProxy_works_for_shared_type_entity_types() + { + using var context = new NeweyContext(); + + Assert.Same(typeof(SharedTypeEntityType), context.Set("STET1").CreateProxy().GetType().BaseType); + Assert.Same(typeof(SharedTypeEntityType), context.Set("STET1").CreateProxy(_ => { }).GetType().BaseType); + } + + [ConditionalFact] + public void CreateProxy_throws_for_shared_type_entity_types_when_entity_type_name_not_known() + { + using var context = new NeweyContext(); + + Assert.Equal( + CoreStrings.EntityTypeNotFoundSharedProxy(nameof(SharedTypeEntityType)), + Assert.Throws(() => context.CreateProxy()).Message); + + Assert.Equal( + CoreStrings.EntityTypeNotFoundSharedProxy(nameof(SharedTypeEntityType)), + Assert.Throws(() => context.CreateProxy(_ => { })).Message); + + Assert.Equal( + CoreStrings.EntityTypeNotFoundSharedProxy(nameof(SharedTypeEntityType)), + Assert.Throws(() => context.CreateProxy(typeof(SharedTypeEntityType))).Message); + } + [ConditionalFact] public void CreateProxy_uses_parameterless_constructor() { @@ -248,6 +275,11 @@ public void Throws_if_attempt_to_add_proxy_type_to_model_builder() }).Message); } + public class SharedTypeEntityType + { + public virtual int Id { get; set; } + } + public class March82GGtp { public virtual int Id { get; set; } @@ -350,6 +382,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(e => e.Id); b.Property(e => e.Sponsor); }); + + modelBuilder.SharedTypeEntity("STET1"); + modelBuilder.SharedTypeEntity("STET2"); } } diff --git a/test/EFCore.Specification.Tests/ManyToManyTrackingTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyTrackingTestBase.cs index 2820e5dc9c8..d1dba97416b 100644 --- a/test/EFCore.Specification.Tests/ManyToManyTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyTrackingTestBase.cs @@ -1898,7 +1898,7 @@ void ValidateFixup(DbContext context, IList leftEntities, IList().Count()); Assert.Equal(3, context.ChangeTracker.Entries().Count()); - Assert.Equal(5, context.ChangeTracker.Entries>().Count()); + Assert.Equal(5, context.ChangeTracker.Entries().Count()); Assert.Equal(3, leftEntities[0].ThreeSkipPayloadFullShared.Count); Assert.Single(leftEntities[1].ThreeSkipPayloadFullShared); @@ -1951,8 +1951,8 @@ public virtual void Can_update_many_to_many_shared_with_payload() context.ChangeTracker.DetectChanges(); } - context.Set>("JoinOneToThreePayloadFullShared").Find(7712, 1)["Payload"] = "Set!"; - context.Set>("JoinOneToThreePayloadFullShared").Find(20, 16)["Payload"] = "Changed!"; + context.Set("JoinOneToThreePayloadFullShared").Find(7712, 1)["Payload"] = "Set!"; + context.Set("JoinOneToThreePayloadFullShared").Find(20, 16)["Payload"] = "Changed!"; ValidateFixup(context, leftEntities, rightEntities, 24, 24, 48, postSave: false); @@ -1979,7 +1979,7 @@ static void ValidateFixup( { Assert.Equal(leftCount, context.ChangeTracker.Entries().Count()); Assert.Equal(rightCount, context.ChangeTracker.Entries().Count()); - Assert.Equal(joinCount, context.ChangeTracker.Entries>().Count()); + Assert.Equal(joinCount, context.ChangeTracker.Entries().Count()); Assert.Equal(leftCount + rightCount + joinCount, context.ChangeTracker.Entries().Count()); Assert.Contains(leftEntities[0].ThreeSkipPayloadFullShared, e => e.Id == 7721); @@ -2040,7 +2040,7 @@ static void ValidateFixup( } } - var deleted = context.ChangeTracker.Entries>().Count(e => e.State == EntityState.Deleted); + var deleted = context.ChangeTracker.Entries().Count(e => e.State == EntityState.Deleted); Assert.Equal(joinCount, (count / 2) + deleted); } } @@ -2996,43 +2996,46 @@ public void Can_insert_update_delete_shared_type_entity_type() ExecuteWithStrategyInTransaction( context => { - var entity = context.Set>("JoinOneToThreePayloadFullShared").CreateInstance( + var entity = context.Set("JoinOneToThreePayloadFullShared").CreateInstance( c => { c["OneId"] = 1; c["ThreeId"] = 1; c["Payload"] = "NewlyAdded"; }); - context.Set>("JoinOneToThreePayloadFullShared").Add(entity); + context.Set("JoinOneToThreePayloadFullShared").Add(entity); context.SaveChanges(); }, context => { - var entity = context.Set>("JoinOneToThreePayloadFullShared") + var entity = context.Set("JoinOneToThreePayloadFullShared") .Single(e => (int)e["OneId"] == 1 && (int)e["ThreeId"] == 1); Assert.Equal("NewlyAdded", (string)entity["Payload"]); entity["Payload"] = "AlreadyUpdated"; - context.Set>("JoinOneToThreePayloadFullShared").Update(entity); + if (RequiresDetectChanges) + { + context.ChangeTracker.DetectChanges(); + } context.SaveChanges(); }, context => { - var entity = context.Set>("JoinOneToThreePayloadFullShared") + var entity = context.Set("JoinOneToThreePayloadFullShared") .Single(e => (int)e["OneId"] == 1 && (int)e["ThreeId"] == 1); Assert.Equal("AlreadyUpdated", (string)entity["Payload"]); - context.Set>("JoinOneToThreePayloadFullShared").Remove(entity); + context.Set("JoinOneToThreePayloadFullShared").Remove(entity); context.SaveChanges(); Assert.False( - context.Set>("JoinOneToThreePayloadFullShared") + context.Set("JoinOneToThreePayloadFullShared") .Any(e => (int)e["OneId"] == 1 && (int)e["ThreeId"] == 1)); }); } diff --git a/test/EFCore.Specification.Tests/Query/ManyToManyQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/ManyToManyQueryFixtureBase.cs index aac3a89a346..e4d738da649 100644 --- a/test/EFCore.Specification.Tests/Query/ManyToManyQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/ManyToManyQueryFixtureBase.cs @@ -152,6 +152,8 @@ public IReadOnlyDictionary GetEntityAsserters() protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { + modelBuilder.SharedTypeEntity("JoinOneToThreePayloadFullShared"); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); @@ -205,7 +207,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity() .HasMany(e => e.ThreeSkipPayloadFullShared) .WithMany(e => e.OneSkipPayloadFullShared) - .UsingEntity>( + .UsingEntity( "JoinOneToThreePayloadFullShared", r => r.HasOne().WithMany(e => e.JoinOnePayloadFullShared).HasForeignKey("ThreeId"), l => l.HasOne().WithMany(e => e.JoinThreePayloadFullShared).HasForeignKey("OneId")) diff --git a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityOne.cs b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityOne.cs index b31aef1ac83..87325aff517 100644 --- a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityOne.cs +++ b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityOne.cs @@ -27,8 +27,8 @@ public class EntityOne public virtual ICollection ThreeSkipPayloadFullShared { get; } = new ObservableCollection(); // #21684 - public virtual ICollection> JoinThreePayloadFullShared { get; } - = new ObservableCollection>(); // #21684 + public virtual ICollection JoinThreePayloadFullShared { get; } + = new ObservableCollection(); // #21684 public virtual ICollection SelfSkipPayloadLeft { get; } = new ObservableCollection(); // #21684 diff --git a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityThree.cs b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityThree.cs index daeae22bc01..45e42651a4e 100644 --- a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityThree.cs +++ b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/EntityThree.cs @@ -28,8 +28,8 @@ public class EntityThree public virtual ICollection OneSkipPayloadFullShared { get; } = new ObservableCollection(); // #21684 - public virtual ICollection> JoinOnePayloadFullShared { get; } - = new ObservableCollection>(); // #21684 + public virtual ICollection JoinOnePayloadFullShared { get; } + = new ObservableCollection(); // #21684 public virtual ICollection CompositeKeySkipFull { get; } = new ObservableCollection(); // #21684 diff --git a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ManyToManyData.cs b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ManyToManyData.cs index 910bba75456..0dbd02e6bae 100644 --- a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ManyToManyData.cs +++ b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ManyToManyData.cs @@ -25,7 +25,7 @@ public class ManyToManyData : ISetSource private readonly JoinTwoToThree[] _joinTwoToThrees; private readonly Dictionary[] _joinOneToTwoShareds; - private readonly Dictionary[] _joinOneToThreePayloadFullShareds; + private readonly ProxyablePropertyBag[] _joinOneToThreePayloadFullShareds; private readonly Dictionary[] _joinTwoSelfShareds; private readonly Dictionary[] _joinTwoToCompositeKeyShareds; private readonly Dictionary[] _joinThreeToRootShareds; @@ -281,8 +281,7 @@ public static void Seed(ManyToManyContext context) context.Set().AddRange(CreateJoinTwoToThrees(context)); context.Set>("EntityOneEntityTwo").AddRange(CreateEntityOneEntityTwos(context)); - context.Set>("JoinOneToThreePayloadFullShared") - .AddRange(CreateJoinOneToThreePayloadFullShareds(context)); + context.Set("JoinOneToThreePayloadFullShared").AddRange(CreateJoinOneToThreePayloadFullShareds(context)); context.Set>("JoinTwoSelfShared").AddRange(CreateJoinTwoSelfShareds(context)); context.Set>("JoinTwoToCompositeKeyShared").AddRange(CreateJoinTwoToCompositeKeyShareds(context)); context.Set>("EntityRootEntityThree").AddRange(CreateEntityRootEntityThrees(context)); @@ -1124,7 +1123,7 @@ private static Dictionary CreateEntityOneEntityTwo( e["EntityTwoId"] = twoId; }); - private static Dictionary[] CreateJoinOneToThreePayloadFullShareds(ManyToManyContext context) + private static ProxyablePropertyBag[] CreateJoinOneToThreePayloadFullShareds(ManyToManyContext context) => new[] { CreateJoinOneToThreePayloadFullShared(context, 3, 1, "Capbrough"), @@ -1169,13 +1168,13 @@ private static Dictionary[] CreateJoinOneToThreePayloadFullShare CreateJoinOneToThreePayloadFullShared(context, 20, 16, "Bayburgh Hills") }; - private static Dictionary CreateJoinOneToThreePayloadFullShared( + private static ProxyablePropertyBag CreateJoinOneToThreePayloadFullShared( ManyToManyContext context, int oneId, int threeId, string payload) => CreateInstance( - context?.Set>("JoinOneToThreePayloadFullShared"), e => + context?.Set("JoinOneToThreePayloadFullShared"), e => { e["OneId"] = oneId; e["ThreeId"] = threeId; diff --git a/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ProxyablePropertyBag.cs b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ProxyablePropertyBag.cs new file mode 100644 index 00000000000..771cc6ccc68 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ManyToManyModel/ProxyablePropertyBag.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel +{ + public class ProxyablePropertyBag : IDictionary + { + private readonly IDictionary _keyValueStore = new Dictionary(); + + public void Add(string key, object value) + => _keyValueStore.Add(key, value); + + public bool ContainsKey(string key) + => _keyValueStore.ContainsKey(key); + + public bool Remove(string key) + => _keyValueStore.Remove(key); + + public bool TryGetValue(string key, out object value) + => _keyValueStore.TryGetValue(key, out value); + + public virtual object this[string key] + { + get => _keyValueStore[key]; + set => _keyValueStore[key] = value; + } + + public ICollection Keys + => _keyValueStore.Keys; + + public ICollection Values + => _keyValueStore.Values; + + public IEnumerator> GetEnumerator() + => _keyValueStore.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void Add(KeyValuePair item) + => _keyValueStore.Add(item); + + public void Clear() + => _keyValueStore.Clear(); + + public bool Contains(KeyValuePair item) + => _keyValueStore.Contains(item); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + => _keyValueStore.CopyTo(array, arrayIndex); + + public bool Remove(KeyValuePair item) + => _keyValueStore.Remove(item); + + public int Count + => _keyValueStore.Count; + + public bool IsReadOnly + => _keyValueStore.IsReadOnly; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs index 68a402bfc7e..9eccecd421a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs @@ -111,7 +111,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasDefaultValueSql("GETUTCDATE()"); modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .SharedTypeEntity("JoinOneToThreePayloadFullShared") .IndexerProperty("Payload") .HasDefaultValue("Generated"); diff --git a/test/EFCore.SqlServer.FunctionalTests/ManyToManyTrackingSqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/ManyToManyTrackingSqlServerTestBase.cs index a88472ec150..e7b4a90cc1e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ManyToManyTrackingSqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ManyToManyTrackingSqlServerTestBase.cs @@ -35,7 +35,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasDefaultValueSql("GETUTCDATE()"); modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .SharedTypeEntity("JoinOneToThreePayloadFullShared") .IndexerProperty("Payload") .HasDefaultValue("Generated"); diff --git a/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs index 30991d1480e..e6d5b5356e8 100644 --- a/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs +++ b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs @@ -30,7 +30,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasDefaultValueSql("CURRENT_TIMESTAMP"); modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .SharedTypeEntity("JoinOneToThreePayloadFullShared") .IndexerProperty("Payload") .HasDefaultValue("Generated"); diff --git a/test/EFCore.Sqlite.FunctionalTests/ManyToManyTrackingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ManyToManyTrackingSqliteTest.cs index eeb8169832d..0b10cd2efd2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/ManyToManyTrackingSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/ManyToManyTrackingSqliteTest.cs @@ -34,7 +34,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasDefaultValueSql("CURRENT_TIMESTAMP"); modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .SharedTypeEntity("JoinOneToThreePayloadFullShared") .IndexerProperty("Payload") .HasDefaultValue("Generated");