diff --git a/src/Microsoft.Data.Entity/ChangeTracking/NavigationFixer.cs b/src/Microsoft.Data.Entity/ChangeTracking/NavigationFixer.cs index 7a1a339c208..a4718f255e2 100644 --- a/src/Microsoft.Data.Entity/ChangeTracking/NavigationFixer.cs +++ b/src/Microsoft.Data.Entity/ChangeTracking/NavigationFixer.cs @@ -2,7 +2,6 @@ // 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.Linq; using JetBrains.Annotations; using Microsoft.Data.Entity.Metadata; @@ -13,8 +12,7 @@ namespace Microsoft.Data.Entity.ChangeTracking public class NavigationFixer : IEntityStateListener { private readonly StateManager _stateManager; - private readonly ClrCollectionAccessorSource _collectionAccessorSource; - private readonly ClrPropertySetterSource _setterSource; + private readonly NavigationAccessorSource _accessorSource; /// /// This constructor is intended only for use when creating test doubles that will override members @@ -27,16 +25,13 @@ protected NavigationFixer() public NavigationFixer( [NotNull] StateManager stateManager, - [NotNull] ClrCollectionAccessorSource collectionAccessorSource, - [NotNull] ClrPropertySetterSource setterSource) + [NotNull] NavigationAccessorSource accessorSource) { Check.NotNull(stateManager, "stateManager"); - Check.NotNull(collectionAccessorSource, "collectionAccessorSource"); - Check.NotNull(setterSource, "setterSource"); + Check.NotNull(accessorSource, "accessorSource"); _stateManager = stateManager; - _collectionAccessorSource = collectionAccessorSource; - _setterSource = setterSource; + _accessorSource = accessorSource; } public virtual void StateChanging(StateEntry entry, EntityState newState) @@ -81,37 +76,33 @@ private void DoFixup(IForeignKey foreignKey, StateEntry principalEntry, StateEnt { foreach (var navigation in _stateManager.Model.GetNavigations(foreignKey)) { + var accessor = _accessorSource.GetAccessor(navigation); + if (navigation.PointsToPrincipal) { - var accessor = _setterSource.GetAccessor(navigation); - foreach (var dependent in dependentEntries) { - accessor.SetClrValue(dependent.Entity, principalEntry.Entity); + accessor.Setter.SetClrValue(dependent.Entity, principalEntry.Entity); } } else { - // TODO: Avoid doing this lookup every time--should be cached - if (navigation.EntityType.Type.GetAnyProperty(navigation.Name).PropertyType.TryGetElementType(typeof(IEnumerable<>)) == null) - { - var accessor = _setterSource.GetAccessor(navigation); - - // TODO: Decide how to handle case where multiple values match non-collection nav prop - accessor.SetClrValue(principalEntry.Entity, dependentEntries.Single().Entity); - } - else + var collectionAccessor = accessor as CollectionNavigationAccessor; + if (collectionAccessor != null) { - var accessor = _collectionAccessorSource.GetAccessor(navigation); - foreach (var dependent in dependentEntries) { - if (!accessor.Contains(principalEntry.Entity, dependent.Entity)) + if (!collectionAccessor.Collection.Contains(principalEntry.Entity, dependent.Entity)) { - accessor.Add(principalEntry.Entity, dependent.Entity); + collectionAccessor.Collection.Add(principalEntry.Entity, dependent.Entity); } } } + else + { + // TODO: Decide how to handle case where multiple values match non-collection nav prop + accessor.Setter.SetClrValue(principalEntry.Entity, dependentEntries.Single().Entity); + } } } } diff --git a/src/Microsoft.Data.Entity/Extensions/EntityServiceCollectionExtensions.cs b/src/Microsoft.Data.Entity/Extensions/EntityServiceCollectionExtensions.cs index c80a3753dd4..2b4d2ac71d3 100644 --- a/src/Microsoft.Data.Entity/Extensions/EntityServiceCollectionExtensions.cs +++ b/src/Microsoft.Data.Entity/Extensions/EntityServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ public static EntityServicesBuilder AddEntityFramework([NotNull] this IServiceCo .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Microsoft.Data.Entity/Metadata/CollectionNavigationAccessor.cs b/src/Microsoft.Data.Entity/Metadata/CollectionNavigationAccessor.cs new file mode 100644 index 00000000000..89eb4683621 --- /dev/null +++ b/src/Microsoft.Data.Entity/Metadata/CollectionNavigationAccessor.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using JetBrains.Annotations; +using Microsoft.Data.Entity.Utilities; + +namespace Microsoft.Data.Entity.Metadata +{ + public class CollectionNavigationAccessor : NavigationAccessor + { + private readonly ThreadSafeLazyRef _collectionAccessor; + + public CollectionNavigationAccessor( + [NotNull] Func getter, + [NotNull] Func setter, + [NotNull] Func collectionAccessor) + : base(getter, setter) + { + Check.NotNull(collectionAccessor, "collectionAccessor"); + + _collectionAccessor = new ThreadSafeLazyRef(collectionAccessor); + } + + public virtual IClrCollectionAccessor Collection + { + get { return _collectionAccessor.Value; } + } + } +} diff --git a/src/Microsoft.Data.Entity/Metadata/NavigationAccessor.cs b/src/Microsoft.Data.Entity/Metadata/NavigationAccessor.cs new file mode 100644 index 00000000000..a387ea179e1 --- /dev/null +++ b/src/Microsoft.Data.Entity/Metadata/NavigationAccessor.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using JetBrains.Annotations; +using Microsoft.Data.Entity.Utilities; + +namespace Microsoft.Data.Entity.Metadata +{ + public class NavigationAccessor + { + private readonly ThreadSafeLazyRef _getter; + private readonly ThreadSafeLazyRef _setter; + + public NavigationAccessor( + [NotNull] Func getter, + [NotNull] Func setter) + { + Check.NotNull(getter, "getter"); + Check.NotNull(setter, "setter"); + + _getter = new ThreadSafeLazyRef(getter); + _setter = new ThreadSafeLazyRef(setter); + } + + public virtual IClrPropertyGetter Getter + { + get { return _getter.Value; } + } + + public virtual IClrPropertySetter Setter + { + get { return _setter.Value; } + } + } +} diff --git a/src/Microsoft.Data.Entity/Metadata/NavigationAccessorSource.cs b/src/Microsoft.Data.Entity/Metadata/NavigationAccessorSource.cs new file mode 100644 index 00000000000..321231d8558 --- /dev/null +++ b/src/Microsoft.Data.Entity/Metadata/NavigationAccessorSource.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// 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 JetBrains.Annotations; +using Microsoft.Data.Entity.Utilities; + +namespace Microsoft.Data.Entity.Metadata +{ + public class NavigationAccessorSource + { + private readonly ThreadSafeDictionaryCache, NavigationAccessor> _cache + = new ThreadSafeDictionaryCache, NavigationAccessor>(); + + private readonly ClrCollectionAccessorSource _collectionAccessorSource; + private readonly ClrPropertySetterSource _setterSource; + private readonly ClrPropertyGetterSource _getterSource; + + public NavigationAccessorSource( + [NotNull] ClrPropertyGetterSource getterSource, + [NotNull] ClrPropertySetterSource setterSource, + [NotNull] ClrCollectionAccessorSource collectionAccessorSource) + { + Check.NotNull(getterSource, "getterSource"); + Check.NotNull(setterSource, "setterSource"); + Check.NotNull(collectionAccessorSource, "collectionAccessorSource"); + + _getterSource = getterSource; + _setterSource = setterSource; + _collectionAccessorSource = collectionAccessorSource; + } + + public virtual NavigationAccessor GetAccessor([NotNull] INavigation navigation) + { + Check.NotNull(navigation, "navigation"); + + return _cache.GetOrAdd( + Tuple.Create(navigation.EntityType.Type, navigation.Name), + k => Create(navigation)); + } + + private NavigationAccessor Create(INavigation navigation) + { + var elementType = navigation.EntityType.Type.GetAnyProperty(navigation.Name).PropertyType.TryGetElementType(typeof(IEnumerable<>)); + + var targetType = navigation.PointsToPrincipal + ? navigation.ForeignKey.ReferencedEntityType + : navigation.ForeignKey.EntityType; + + // TODO: Consider allowing an annotation to force treating a reference to an Entity which is an IEnumerable of + // itself as a reference instead of a collection. Currently it will be considered a collection nav prop, which it isn't. + + return elementType != null && elementType == targetType.Type + ? new CollectionNavigationAccessor( + () => _getterSource.GetAccessor(navigation), + () => _setterSource.GetAccessor(navigation), + () => _collectionAccessorSource.GetAccessor(navigation)) + : new NavigationAccessor( + () => _getterSource.GetAccessor(navigation), + () => _setterSource.GetAccessor(navigation)); + } + } +} diff --git a/src/Microsoft.Data.Entity/Microsoft.Data.Entity.csproj b/src/Microsoft.Data.Entity/Microsoft.Data.Entity.csproj index db576bac530..9dbb37d2311 100644 --- a/src/Microsoft.Data.Entity/Microsoft.Data.Entity.csproj +++ b/src/Microsoft.Data.Entity/Microsoft.Data.Entity.csproj @@ -75,6 +75,9 @@ + + + diff --git a/test/Microsoft.Data.Entity.Tests/ChangeTracking/NavigationFixerTest.cs b/test/Microsoft.Data.Entity.Tests/ChangeTracking/NavigationFixerTest.cs index 6fd6bc08216..73552b7c9b9 100644 --- a/test/Microsoft.Data.Entity.Tests/ChangeTracking/NavigationFixerTest.cs +++ b/test/Microsoft.Data.Entity.Tests/ChangeTracking/NavigationFixerTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using Microsoft.Data.Entity.ChangeTracking; using Microsoft.Data.Entity.Metadata; @@ -15,8 +16,7 @@ public class NavigationFixerTest [Fact] public void Members_check_arguments() { - var fixer = new NavigationFixer( - Mock.Of(), new ClrCollectionAccessorSource(), new ClrPropertySetterSource()); + var fixer = new NavigationFixer(Mock.Of(), CreateAccessorSource()); Assert.Equal( "entry", @@ -42,7 +42,7 @@ public void Does_fixup_of_related_principals() var dependentEntry = manager.GetOrCreateEntry(dependent); manager.StartTracking(dependentEntry); - var fixer = new NavigationFixer(manager, new ClrCollectionAccessorSource(), new ClrPropertySetterSource()); + var fixer = new NavigationFixer(manager, CreateAccessorSource()); fixer.StateChanged(dependentEntry, EntityState.Unknown); Assert.Same(dependent.Category, principal2); @@ -68,7 +68,7 @@ public void Does_fixup_of_related_dependents() var principalEntry = manager.GetOrCreateEntry(principal); manager.StartTracking(principalEntry); - var fixer = new NavigationFixer(manager, new ClrCollectionAccessorSource(), new ClrPropertySetterSource()); + var fixer = new NavigationFixer(manager, CreateAccessorSource()); fixer.StateChanged(principalEntry, EntityState.Unknown); Assert.Same(dependent1.Category, principal); @@ -80,6 +80,114 @@ public void Does_fixup_of_related_dependents() Assert.Contains(dependent3, principal.Products); } + [Fact] + public void Does_fixup_of_one_to_one_relationship() + { + var manager = CreateStateManager(); + + var principal1 = new Product { Id = 21 }; + var principal2 = new Product { Id = 22 }; + var principal3 = new Product { Id = 23 }; + + var dependent1 = new ProductDetail { Id = 21 }; + var dependent2 = new ProductDetail { Id = 22 }; + var dependent4 = new ProductDetail { Id = 24 }; + + var principalEntry1 = manager.GetOrCreateEntry(principal1); + var principalEntry2 = manager.GetOrCreateEntry(principal2); + var principalEntry3 = manager.GetOrCreateEntry(principal3); + + var dependentEntry1 = manager.GetOrCreateEntry(dependent1); + var dependentEntry2 = manager.GetOrCreateEntry(dependent2); + var dependentEntry4 = manager.GetOrCreateEntry(dependent4); + + manager.StartTracking(principalEntry1); + manager.StartTracking(principalEntry2); + manager.StartTracking(principalEntry3); + + manager.StartTracking(dependentEntry1); + manager.StartTracking(dependentEntry2); + manager.StartTracking(dependentEntry4); + + var fixer = new NavigationFixer(manager, CreateAccessorSource()); + + Assert.Null(principal1.Detail); + Assert.Null(dependent1.Product); + + fixer.StateChanged(principalEntry1, EntityState.Unknown); + + Assert.Same(principal1, dependent1.Product); + Assert.Same(dependent1, principal1.Detail); + + Assert.Null(principal2.Detail); + Assert.Null(dependent2.Product); + + fixer.StateChanged(dependentEntry2, EntityState.Unknown); + + Assert.Same(principal2, dependent2.Product); + Assert.Same(dependent2, principal2.Detail); + + Assert.Null(principal3.Detail); + Assert.Null(dependent4.Product); + + fixer.StateChanged(principalEntry3, EntityState.Unknown); + fixer.StateChanged(dependentEntry4, EntityState.Unknown); + + Assert.Null(principal3.Detail); + Assert.Null(dependent4.Product); + } + + [Fact] + public void Does_fixup_of_one_to_one_self_referencing_relationship() + { + var manager = CreateStateManager(); + + var entity1 = new Product { Id = 21, AlternateProductId = 22 }; + var entity2 = new Product { Id = 22, AlternateProductId = 23 }; + var entity3 = new Product { Id = 23 }; + + var entry1 = manager.GetOrCreateEntry(entity1); + var entry2 = manager.GetOrCreateEntry(entity2); + var entry3 = manager.GetOrCreateEntry(entity3); + + manager.StartTracking(entry1); + manager.StartTracking(entry2); + manager.StartTracking(entry3); + + var fixer = new NavigationFixer(manager, CreateAccessorSource()); + + Assert.Null(entity1.AlternateProduct); + Assert.Null(entity1.OriginalProduct); + + Assert.Null(entity2.AlternateProduct); + Assert.Null(entity2.OriginalProduct); + + Assert.Null(entity3.AlternateProduct); + Assert.Null(entity3.OriginalProduct); + + fixer.StateChanged(entry1, EntityState.Unknown); + + Assert.Same(entity2, entity1.AlternateProduct); + Assert.Null(entity1.OriginalProduct); + + Assert.Null(entity2.AlternateProduct); + Assert.Same(entity1, entity2.OriginalProduct); + + Assert.Null(entity3.AlternateProduct); + Assert.Null(entity3.OriginalProduct); + + fixer.StateChanged(entry3, EntityState.Unknown); + + Assert.Same(entity2, entity1.AlternateProduct); + Assert.Null(entity1.OriginalProduct); + + Assert.Same(entity3, entity2.AlternateProduct); + Assert.Same(entity1, entity2.OriginalProduct); + + Assert.Null(entity3.AlternateProduct); + Assert.Same(entity2, entity3.OriginalProduct); + } + private static StateManager CreateStateManager() { return TestHelpers.CreateContextConfiguration(BuildModel()).Services.StateManager; @@ -103,6 +211,29 @@ private class Product public int CategoryId { get; set; } public Category Category { get; set; } + + public ProductDetail Detail { get; set; } + + public int? AlternateProductId { get; set; } + public Product AlternateProduct { get; set; } + public Product OriginalProduct { get; set; } + } + + private class ProductDetail : IEnumerable + { + public int Id { get; set; } + + public Product Product { get; set; } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } private static IModel BuildModel() @@ -112,19 +243,31 @@ private static IModel BuildModel() builder.Entity(); builder.Entity(); + builder.Entity(); var categoryType = model.GetEntityType(typeof(Category)); var productType = model.GetEntityType(typeof(Product)); + var productDetailType = model.GetEntityType(typeof(ProductDetail)); + + var categoryFk = productType.AddForeignKey(categoryType.GetKey(), productType.GetProperty("CategoryId")); + var alternateProductFk = productType.AddForeignKey(productType.GetKey(), productType.GetProperty("AlternateProductId")); + var productDetailFk = productDetailType.AddForeignKey(productType.GetKey(), productDetailType.GetProperty("Id")); - var categoryIdFk - = productType.AddForeignKey(categoryType.GetKey(), productType.GetProperty("CategoryId")); + categoryType.AddNavigation(new Navigation(categoryFk, "Products", pointsToPrincipal: false)); + productType.AddNavigation(new Navigation(categoryFk, "Category", pointsToPrincipal: true)); - categoryIdFk.StorageName = "Category_Products"; + productType.AddNavigation(new Navigation(alternateProductFk, "AlternateProduct", pointsToPrincipal: true)); + productType.AddNavigation(new Navigation(alternateProductFk, "OriginalProduct", pointsToPrincipal: false)); - categoryType.AddNavigation(new Navigation(categoryIdFk, "Products", pointsToPrincipal: false)); - productType.AddNavigation(new Navigation(categoryIdFk, "Category", pointsToPrincipal: true)); + productDetailType.AddNavigation(new Navigation(productDetailFk, "Product", pointsToPrincipal: true)); + productType.AddNavigation(new Navigation(productDetailFk, "Detail", pointsToPrincipal: false)); return model; } + + private static NavigationAccessorSource CreateAccessorSource() + { + return new NavigationAccessorSource(new ClrPropertyGetterSource(), new ClrPropertySetterSource(), new ClrCollectionAccessorSource()); + } } } diff --git a/test/Microsoft.Data.Entity.Tests/Metadata/NavigationAccessorSourceTest.cs b/test/Microsoft.Data.Entity.Tests/Metadata/NavigationAccessorSourceTest.cs new file mode 100644 index 00000000000..ac00ef01409 --- /dev/null +++ b/test/Microsoft.Data.Entity.Tests/Metadata/NavigationAccessorSourceTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Data.Entity.Metadata; +using Xunit; + +namespace Microsoft.Data.Entity.Tests.Metadata +{ + public class NavigationAccessorSourceTest + { + [Fact] + public void Creates_collection_accessor_for_appropriate_IEnumerable_property() + { + var model = BuildModel(); + var source = CreateAccessorSource(); + + var productsNavigation = model.GetEntityType(typeof(Category)).Navigations.Single(n => n.Name == "Products"); + Assert.IsType(source.GetAccessor(productsNavigation)); + + var categoriesNavigation = model.GetEntityType(typeof(Product)).Navigations.Single(n => n.Name == "Categories"); + Assert.IsType(source.GetAccessor(categoriesNavigation)); + } + + [Fact] + public void Creates_reference_accessor_for_non_matching_IEnumerable_property() + { + var model = BuildModel(); + var source = CreateAccessorSource(); + + var productNavigation = model.GetEntityType(typeof(Category)).Navigations.Single(n => n.Name == "Product"); + Assert.IsType(source.GetAccessor(productNavigation)); + + var categoryNavigation = model.GetEntityType(typeof(Product)).Navigations.Single(n => n.Name == "Category"); + Assert.IsType(source.GetAccessor(categoryNavigation)); + } + + [Fact] + public void Caches_accessor_for_given_property_on_given_type() + { + var model = BuildModel(); + var source = CreateAccessorSource(); + + var productNavigation = model.GetEntityType(typeof(Category)).Navigations.Single(n => n.Name == "Product"); + + Assert.Same(source.GetAccessor(productNavigation), source.GetAccessor(productNavigation)); + } + + private static NavigationAccessorSource CreateAccessorSource() + { + return new NavigationAccessorSource(new ClrPropertyGetterSource(), new ClrPropertySetterSource(), new ClrCollectionAccessorSource()); + } + + private class Category : IEnumerable + { + public int Id { get; set; } + + public IEnumerable Products { get; set; } + + public int ProductId { get; set; } + public Product Product { get; set; } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private class Product : IEnumerable + { + public int Id { get; set; } + + public int CategoryId { get; set; } + public Category Category { get; set; } + + public List Categories { get; set; } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private static IModel BuildModel() + { + var model = new Model(); + var builder = new ConventionModelBuilder(model); + + builder.Entity(); + builder.Entity(); + + var categoryType = model.GetEntityType(typeof(Category)); + var productType = model.GetEntityType(typeof(Product)); + + var categoryFk = productType.AddForeignKey(categoryType.GetKey(), productType.GetProperty("CategoryId")); + var productFk = categoryType.AddForeignKey(productType.GetKey(), categoryType.GetProperty("ProductId")); + + categoryType.AddNavigation(new Navigation(categoryFk, "Products", pointsToPrincipal: false)); + productType.AddNavigation(new Navigation(categoryFk, "Category", pointsToPrincipal: true)); + + productType.AddNavigation(new Navigation(productFk, "Categories", pointsToPrincipal: false)); + categoryType.AddNavigation(new Navigation(productFk, "Product", pointsToPrincipal: true)); + + return model; + } + + } +} diff --git a/test/Microsoft.Data.Entity.Tests/Microsoft.Data.Entity.Tests.csproj b/test/Microsoft.Data.Entity.Tests/Microsoft.Data.Entity.Tests.csproj index 965d01420f8..10c4fca22cc 100644 --- a/test/Microsoft.Data.Entity.Tests/Microsoft.Data.Entity.Tests.csproj +++ b/test/Microsoft.Data.Entity.Tests/Microsoft.Data.Entity.Tests.csproj @@ -84,6 +84,7 @@ +