diff --git a/Build/nuget-symbols.cmd b/Build/nuget-symbols.cmd index 05d76184..da0dbec5 100644 --- a/Build/nuget-symbols.cmd +++ b/Build/nuget-symbols.cmd @@ -1,11 +1,12 @@ rem Pushing Symbols packages to NuGet.org -set version=2.5.0-alpha4 -NuGet.exe SetApiKey 5682793e-2994-4016-b7b4-c11be576703b -NuGet.exe push Output\TrackableEntities.Common\TrackableEntities.Common.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.Client\TrackableEntities.Client.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.Client.Net4\TrackableEntities.Client.Net4.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.EF.5\TrackableEntities.EF.5.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.EF.6\TrackableEntities.EF.6.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.Patterns\TrackableEntities.Patterns.%version%.symbols.nupkg -NuGet.exe push Output\TrackableEntities.Patterns.EF.6\TrackableEntities.Patterns.EF.6.%version%.symbols.nupkg +set version=2.5.7 +set source=https://nuget.smbsrc.net/ +nuget SetApiKey 5682793e-2994-4016-b7b4-c11be576703b +nuget push Output\TrackableEntities.Common\TrackableEntities.Common.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.Client\TrackableEntities.Client.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.Client.Net4\TrackableEntities.Client.Net4.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.EF.5\TrackableEntities.EF.5.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.EF.6\TrackableEntities.EF.6.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.Patterns\TrackableEntities.Patterns.%version%.symbols.nupkg -src %source% +nuget push Output\TrackableEntities.Patterns.EF.6\TrackableEntities.Patterns.EF.6.%version%.symbols.nupkg -src %source% diff --git a/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.nupkg new file mode 100644 index 00000000..7e35c98f Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.symbols.nupkg new file mode 100644 index 00000000..d3f20e7c Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Client.Net4/TrackableEntities.Client.Net4.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.nupkg new file mode 100644 index 00000000..94c309b9 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.symbols.nupkg new file mode 100644 index 00000000..bf93e249 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Client/TrackableEntities.Client.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.nupkg new file mode 100644 index 00000000..944ec168 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.symbols.nupkg new file mode 100644 index 00000000..f885cdec Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Common/TrackableEntities.Common.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.nupkg new file mode 100644 index 00000000..3dfe99c0 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.symbols.nupkg new file mode 100644 index 00000000..fd2a7bea Binary files /dev/null and b/NuGet/Packages/TrackableEntities.EF.5/TrackableEntities.EF.5.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.nupkg new file mode 100644 index 00000000..6deda2dd Binary files /dev/null and b/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.symbols.nupkg new file mode 100644 index 00000000..ed17bae1 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.EF.6/TrackableEntities.EF.6.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.nupkg new file mode 100644 index 00000000..4555b718 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.symbols.nupkg new file mode 100644 index 00000000..14748d92 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Patterns.EF.6/TrackableEntities.Patterns.EF.6.2.5.7.symbols.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.nupkg b/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.nupkg new file mode 100644 index 00000000..0809bf5c Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.nupkg differ diff --git a/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.symbols.nupkg b/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.symbols.nupkg new file mode 100644 index 00000000..9014e5e1 Binary files /dev/null and b/NuGet/Packages/TrackableEntities.Patterns/TrackableEntities.Patterns.2.5.7.symbols.nupkg differ diff --git a/ReadMe.md b/ReadMe.md index 2a8ffe4e..19c40f59 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,7 +1,7 @@ # **Trackable Entities** ## N-Tier Support for Entity Framework with WCF or ASP.NET Web API -[![Build Status](https://www.myget.org/BuildSource/Badge/trackable-entities-ci?identifier=3e829f78-43c0-48e7-804b-a1381797fbc3)](https://www.myget.org/) +[![trackable-entities-ci MyGet Build Status](https://www.myget.org/BuildSource/Badge/trackable-entities-ci?identifier=3e829f78-43c0-48e7-804b-a1381797fbc3)](https://www.myget.org/F/trackable-entities-ci) [![Join the chat at https://gitter.im/TrackableEntities/trackable-entities](https://badges.gitter.im/TrackableEntities/trackable-entities.svg)](https://gitter.im/TrackableEntities/trackable-entities?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/Contexts/FamilyDbContext.cs b/Source/Tests/TrackableEntities.EF.5.Tests/Contexts/FamilyDbContext.cs index 2932c381..6a2308c7 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/Contexts/FamilyDbContext.cs +++ b/Source/Tests/TrackableEntities.EF.5.Tests/Contexts/FamilyDbContext.cs @@ -28,14 +28,13 @@ public FamilyDbContext(CreateDbOptions createDbOptions = CreateDbOptions.CreateD Database.SetInitializer(new CreateDatabaseIfNotExists()); break; } - //Parents = Set(); - //Children = Set(); } public DbSet Parents { get; set; } public DbSet Children { get; set; } public DbSet Contacts { get; set; } - public DbSet ContactDetails { get; set; } + public DbSet ContactCategories { get; set; } + public DbSet ContactDatas { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/Contact.cs b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/Contact.cs index 479135b3..f19d60f1 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/Contact.cs +++ b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/Contact.cs @@ -9,10 +9,12 @@ public class Contact : ITrackable [Key] public int Id { get; set; } - [ForeignKey(nameof(ContactDetail))] - public int ContactDetailId { get; set; } + [ForeignKey(nameof(ContactCategory))] + public int ContactCategoryId { get; set; } - public ContactDetail ContactDetail { get; set; } + public ContactCategory ContactCategory { get; set; } + + public ContactData ContactData { get; set; } [NotMapped] public TrackingState TrackingState { get; set; } diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactDetail.cs b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactCategory.cs similarity index 81% rename from Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactDetail.cs rename to Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactCategory.cs index d9c766b2..5926f429 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactDetail.cs +++ b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactCategory.cs @@ -4,12 +4,13 @@ namespace TrackableEntities.EF.Tests.FamilyModels { - public class ContactDetail : ITrackable + public class ContactCategory : ITrackable { [Key] public int Id { get; set; } - public string Data { get; set; } + public string CategoryName { get; set; } + [NotMapped] public TrackingState TrackingState { get; set; } diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactData.cs b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactData.cs new file mode 100644 index 00000000..7d273b5b --- /dev/null +++ b/Source/Tests/TrackableEntities.EF.5.Tests/FamilyModels/ContactData.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TrackableEntities.EF.Tests.FamilyModels +{ + public class ContactData : ITrackable + { + [Key, ForeignKey(nameof(Contact))] + public int ContactId { get; set; } + + public string Data { get; set; } + + public Contact Contact { get; set; } + + [NotMapped] + public TrackingState TrackingState { get; set; } + + [NotMapped] + public ICollection ModifiedProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/Helpers/TestsHelper.cs b/Source/Tests/TrackableEntities.EF.5.Tests/Helpers/TestsHelper.cs index 4f5c0819..ded62702 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/Helpers/TestsHelper.cs +++ b/Source/Tests/TrackableEntities.EF.5.Tests/Helpers/TestsHelper.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections; -using System.Linq; -using System.Collections.Generic; -using System.Data; -using System.Data.Entity; +using System.Linq; using TrackableEntities.EF.Tests.Contexts; using Xunit; diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/LoadRelatedEntitiesTests.cs b/Source/Tests/TrackableEntities.EF.5.Tests/LoadRelatedEntitiesTests.cs index 7ba4b754..8e1d09c4 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/LoadRelatedEntitiesTests.cs +++ b/Source/Tests/TrackableEntities.EF.5.Tests/LoadRelatedEntitiesTests.cs @@ -1,21 +1,17 @@ using System; using System.Collections.Generic; -using System.Data; using System.Data.Entity; using System.Data.Entity.Infrastructure; -using System.Data.Entity.ModelConfiguration.Configuration; using System.Linq; +using System.Threading.Tasks; using Xunit; -using TrackableEntities.EF.Tests.Contexts; #if EF_6 -using TrackableEntities.EF6; -using System.Data.Entity.Core.EntityClient; +using System.Data.Entity.Core; #else -using TrackableEntities.EF5; -using System.Data.EntityClient; +using System.Data; #endif using TrackableEntities.EF.Tests; -using TrackableEntities.EF.Tests.Mocks; +using TrackableEntities.EF.Tests.Contexts; using TrackableEntities.EF.Tests.NorthwindModels; using TrackableEntities.EF.Tests.FamilyModels; @@ -337,25 +333,36 @@ private List CreateTestProductsWithProductInfo(NorthwindDbContext conte private List CreateTestContactsWithDetails(FamilyDbContext context) { // Create test entities - var detail1 = new ContactDetail + var category1 = new ContactCategory { - Data = "Foo", + CategoryName = "Friends", }; var contact1 = new Contact { - ContactDetail = detail1 + ContactCategory = category1 }; // Persist entities context.Contacts.Add(contact1); context.SaveChanges(); + // Create one-to-one related entity + var data1 = new ContactData + { + Data = "Data 1", + ContactId = contact1.Id, + Contact = contact1 + }; + context.ContactDatas.Add(data1); + context.SaveChanges(); + // Detach entities var objContext = ((IObjectContextAdapter)context).ObjectContext; objContext.Detach(contact1); // Clear reference properties - contact1.ContactDetail = null; + contact1.ContactCategory = null; + contact1.ContactData = null; // Return entities return new List { contact1 }; @@ -860,24 +867,185 @@ public async void LoadRelatedEntitiesAsync_Should_Populate_Multiple_Employees_Te #endregion - #region Contact-ContactDetails: Non-Matching ForeignKeys + #region LoadRelatedEntities Callbacks + + [Fact] + public void LoadRelatedEntities_OnLoading_Should_Continue() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + context.Contacts.Attach(contact); + + // Act + context.LoadRelatedEntities(contact, true, entities => + { + contact.TrackingState = TrackingState.Added; + return true; + }, (ex, entities) => true); + + // Assert + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.Equal(TrackingState.Added, contact.TrackingState); + } + + [Fact] + public void LoadRelatedEntities_OnLoading_Should_Replace() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + context.Contacts.Attach(contact); + + // Act + context.LoadRelatedEntities(contact, true, entities => + { + var contact1 = entities.OfType().FirstOrDefault(e => e.Id == contact.Id); + if (contact1 != null) + { + context.Entry(contact1).Reference(e => e.ContactCategory).Load(); + context.Entry(contact1).Reference(e => e.ContactData).Load(); + return false; + } + return true; + }); + + // Assert + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.NotNull(contact.ContactData); + Assert.Equal(contact.Id, contact.ContactData.ContactId); + } + + [Fact] + public void LoadRelatedEntities_OnError_Should_Throw() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + + // Act and Assert + Assert.Throws(() => + { + context.LoadRelatedEntities(contact, true, null, (ex, entities) => false); + }); + } [Fact] - public void LoadRelatedEntities_Should_Populate_Entities_With_NonMatching_Foreign_Keys() + public void LoadRelatedEntities_OnError_Should_Continue() { // Arrange var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); var contact = CreateTestContactsWithDetails(context)[0]; - contact.TrackingState = TrackingState.Added; + context.Contacts.Attach(contact); // Act - context.LoadRelatedEntities(contact); + context.LoadRelatedEntities(contact, true, null, (ex, entities) => + { + var contact1 = entities.OfType().FirstOrDefault(e => e.Id == contact.Id); + if (contact1 != null) + context.Entry(contact1).Reference(e => e.ContactData).Load(); + return true; + }); // Assert - Assert.NotNull(contact.ContactDetail); - Assert.Equal(contact.ContactDetailId, contact.ContactDetail.Id); + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.NotNull(contact.ContactData); + Assert.Equal(contact.Id, contact.ContactData.ContactId); } +#if EF_6 + [Fact] + public async Task LoadRelatedEntitiesAsync_OnLoading_Should_Continue() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + context.Contacts.Attach(contact); + + // Act + await context.LoadRelatedEntitiesAsync(contact, true, entities => + { + contact.TrackingState = TrackingState.Added; + return Task.FromResult(true); + }, (ex, entities) => Task.FromResult(true)); + + // Assert + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.Equal(TrackingState.Added, contact.TrackingState); + } + + [Fact] + public async Task LoadRelatedEntitiesAsync_OnLoading_Should_Replace() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + context.Contacts.Attach(contact); + + // Act + await context.LoadRelatedEntitiesAsync(contact, true, async entities => + { + var contact1 = entities.OfType().FirstOrDefault(e => e.Id == contact.Id); + if (contact1 != null) + { + await context.Entry(contact1).Reference(e => e.ContactCategory).LoadAsync(); + await context.Entry(contact1).Reference(e => e.ContactData).LoadAsync(); + return false; + } + return true; + }); + + // Assert + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.NotNull(contact.ContactData); + Assert.Equal(contact.Id, contact.ContactData.ContactId); + } + + [Fact] + public async Task LoadRelatedEntitiesAsync_OnError_Should_Throw() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + + // Act and Assert + await Assert.ThrowsAsync(async () => + { + await context.LoadRelatedEntitiesAsync(contact, true, null, + (ex, entities) => Task.FromResult(false)); + }); + } + + [Fact] + public async Task LoadRelatedEntitiesAsync_OnError_Should_Continue() + { + // Arrange + var context = TestsHelper.CreateFamilyDbContext(CreateFamilyDbOptions); + var contact = CreateTestContactsWithDetails(context)[0]; + context.Contacts.Attach(contact); + + // Act + await context.LoadRelatedEntitiesAsync(contact, true, null, async (ex, entities) => + { + var contact1 = entities.OfType().FirstOrDefault(e => e.Id == contact.Id); + if (contact1 != null) + await context.Entry(contact1).Reference(e => e.ContactData).LoadAsync(); + return true; + }); + + // Assert + Assert.NotNull(contact.ContactCategory); + Assert.Equal(contact.ContactCategoryId, contact.ContactCategory.Id); + Assert.NotNull(contact.ContactData); + Assert.Equal(contact.Id, contact.ContactData.ContactId); + } +#endif + #endregion #region Complex Types @@ -916,8 +1084,8 @@ public async void LoadRelatedEntitiesAsync_With_Complex_Types() // previous call throws exception if not implemented properly. Assert.True(true); } -#endif +#endif -#endregion Complex Types + #endregion } } diff --git a/Source/Tests/TrackableEntities.EF.5.Tests/TrackableEntities.EF.5.Tests.csproj b/Source/Tests/TrackableEntities.EF.5.Tests/TrackableEntities.EF.5.Tests.csproj index 53a0270f..7aec7a33 100644 --- a/Source/Tests/TrackableEntities.EF.5.Tests/TrackableEntities.EF.5.Tests.csproj +++ b/Source/Tests/TrackableEntities.EF.5.Tests/TrackableEntities.EF.5.Tests.csproj @@ -71,7 +71,8 @@ - + + diff --git a/Source/Tests/TrackableEntities.EF.6.Tests/FamilyModels/ContactData.cs b/Source/Tests/TrackableEntities.EF.6.Tests/FamilyModels/ContactData.cs new file mode 100644 index 00000000..7d273b5b --- /dev/null +++ b/Source/Tests/TrackableEntities.EF.6.Tests/FamilyModels/ContactData.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TrackableEntities.EF.Tests.FamilyModels +{ + public class ContactData : ITrackable + { + [Key, ForeignKey(nameof(Contact))] + public int ContactId { get; set; } + + public string Data { get; set; } + + public Contact Contact { get; set; } + + [NotMapped] + public TrackingState TrackingState { get; set; } + + [NotMapped] + public ICollection ModifiedProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/Tests/TrackableEntities.EF.6.Tests/TrackableEntities.EF.6.Tests.csproj b/Source/Tests/TrackableEntities.EF.6.Tests/TrackableEntities.EF.6.Tests.csproj index b217d9ed..31e9c98c 100644 --- a/Source/Tests/TrackableEntities.EF.6.Tests/TrackableEntities.EF.6.Tests.csproj +++ b/Source/Tests/TrackableEntities.EF.6.Tests/TrackableEntities.EF.6.Tests.csproj @@ -96,8 +96,8 @@ FamilyModels\Contact.cs - - FamilyModels\ContactDetail.cs + + FamilyModels\ContactCategory.cs FamilyModels\Parent.cs @@ -174,6 +174,7 @@ TrackableExtensionsTests.cs + diff --git a/Source/TrackableEntities.Client/ChangeTrackingCollection.cs b/Source/TrackableEntities.Client/ChangeTrackingCollection.cs index 653de6f7..c7ed4a3a 100644 --- a/Source/TrackableEntities.Client/ChangeTrackingCollection.cs +++ b/Source/TrackableEntities.Client/ChangeTrackingCollection.cs @@ -165,9 +165,8 @@ private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) || entity.TrackingState == TrackingState.Modified) { if (entity.ModifiedProperties == null) - entity.ModifiedProperties = new List(); - if (!entity.ModifiedProperties.Contains(e.PropertyName)) - entity.ModifiedProperties.Add(e.PropertyName); + entity.ModifiedProperties = new HashSet(); + entity.ModifiedProperties.Add(e.PropertyName); } } } diff --git a/Source/TrackableEntities.EF.5/DbContextExtensions.cs b/Source/TrackableEntities.EF.5/DbContextExtensions.cs index df717327..03c1fc9b 100644 --- a/Source/TrackableEntities.EF.5/DbContextExtensions.cs +++ b/Source/TrackableEntities.EF.5/DbContextExtensions.cs @@ -5,6 +5,7 @@ using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Threading; using TrackableEntities.Common; #if EF_6 @@ -271,10 +272,12 @@ where GetEntityTypes(dbContext, entityType).Contains(es.ElementType) /// Used to query and save changes to a database /// Object that implement ITrackable /// True to load all related entities, false to load only added entities - public static void LoadRelatedEntities(this DbContext context, - ITrackable item, bool loadAll = false) + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception + public static void LoadRelatedEntities(this DbContext context, ITrackable item, bool loadAll = false, + Func, bool> onLoading = null, Func, bool> onError = null) { - LoadRelatedEntities(context, new[] {item}, null, CreateVisitationHelperWithIdMatching(context), loadAll); + LoadRelatedEntities(context, new[] {item}, null, CreateVisitationHelperWithIdMatching(context), loadAll, onLoading, onError); } /// @@ -283,10 +286,12 @@ public static void LoadRelatedEntities(this DbContext context, /// Used to query and save changes to a database /// Objects that implements ITrackable /// True to load all related entities, false to load only added entities + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception public static void LoadRelatedEntities(this DbContext context, IEnumerable items, - bool loadAll = false) + bool loadAll = false, Func, bool> onLoading = null, Func, bool> onError = null) { - LoadRelatedEntities(context, items, null, CreateVisitationHelperWithIdMatching(context), loadAll); + LoadRelatedEntities(context, items, null, CreateVisitationHelperWithIdMatching(context), loadAll, onLoading, onError); } #if EF_6 @@ -296,12 +301,15 @@ public static void LoadRelatedEntities(this DbContext context, IEnumerableUsed to query and save changes to a database /// Object that implement ITrackable /// True to load all related entities, false to load only added entities + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception /// A task that represents the asynchronous operation public static Task LoadRelatedEntitiesAsync(this DbContext context, - ITrackable item, bool loadAll = false) + ITrackable item, bool loadAll = false, + Func, Task> onLoading = null, Func, Task> onError = null) { return LoadRelatedEntitiesAsync(context, new[] {item}, null, CreateVisitationHelperWithIdMatching(context), - CancellationToken.None, loadAll); + CancellationToken.None, loadAll, onLoading, onError); } /// @@ -311,12 +319,15 @@ public static Task LoadRelatedEntitiesAsync(this DbContext context, /// Object that implement ITrackable /// A CancellationToken to observe while waiting for the task to complete. /// True to load all related entities, false to load only added entities + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception /// A task that represents the asynchronous operation. public static Task LoadRelatedEntitiesAsync(this DbContext context, - ITrackable item, CancellationToken cancellationToken, bool loadAll = false) + ITrackable item, CancellationToken cancellationToken, bool loadAll = false, + Func, Task> onLoading = null, Func, Task> onError = null) { return LoadRelatedEntitiesAsync(context, new[] { item }, null, CreateVisitationHelperWithIdMatching(context), - cancellationToken, loadAll); + cancellationToken, loadAll, onLoading, onError); } /// @@ -325,12 +336,15 @@ public static Task LoadRelatedEntitiesAsync(this DbContext context, /// Used to query and save changes to a database /// Objects that implements ITrackable /// True to load all related entities, false to load only added entities + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception /// A task that represents the asynchronous operation. public static Task LoadRelatedEntitiesAsync(this DbContext context, - IEnumerable items, bool loadAll = false) + IEnumerable items, bool loadAll = false, + Func, Task> onLoading = null, Func, Task> onError = null) { return LoadRelatedEntitiesAsync(context, items, null, CreateVisitationHelperWithIdMatching(context), - CancellationToken.None, loadAll); + CancellationToken.None, loadAll, onLoading, onError); } /// @@ -340,17 +354,21 @@ public static Task LoadRelatedEntitiesAsync(this DbContext context, /// Objects that implements ITrackable /// A CancellationToken to observe while waiting for the task to complete. /// True to load all related entities, false to load only added entities + /// Return true to load related entities after preprocessing, false to skip or replace loading. + /// Return true to continue, false to throw exception /// A task that represents the asynchronous operation. public static Task LoadRelatedEntitiesAsync(this DbContext context, IEnumerable items, - CancellationToken cancellationToken, bool loadAll = false) + CancellationToken cancellationToken, bool loadAll = false, + Func, Task> onLoading = null, Func, Task> onError = null) { return LoadRelatedEntitiesAsync(context, items, null, CreateVisitationHelperWithIdMatching(context), - cancellationToken, loadAll); + cancellationToken, loadAll, onLoading, onError); } #endif private static void LoadRelatedEntities(this DbContext context, - IEnumerable items, ITrackable parent, ObjectVisitationHelper visitationHelper, bool loadAll) + IEnumerable items, ITrackable parent, ObjectVisitationHelper visitationHelper, bool loadAll, + Func, bool> onLoading, Func, bool> onError) { // Return if no items if (items == null) return; @@ -362,31 +380,35 @@ private static void LoadRelatedEntities(this DbContext context, var entities = selectedItems.Cast() .Where(i => !IsComplexType(context, i.GetType()) && !visitationHelper.IsVisited(i)); - // Collection 'items' can contain entities of different types (due to inheritance) - // We collect a superset of all properties of all items of type ITrackable - var allProps = (from entity in entities - from prop in entity.GetType().GetProperties() - where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) - select prop).Distinct(); - - // Populate related entities on all items - foreach (var prop in allProps) + // Pre-loading hook + if (onLoading == null || onLoading(entities.OfType())) { - // Get related entities - string propertyName = prop.Name; - Type propertyType = prop.PropertyType; - IEnumerable relatedEntities = context.GetRelatedEntities(entities, - prop.DeclaringType, propertyName, propertyType); + // Collection 'items' can contain entities of different types (due to inheritance) + // We collect a superset of all properties of all items of type ITrackable + var allProps = (from entity in entities + from prop in entity.GetType().GetProperties() + where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) + select prop).Distinct(); + + // Populate related entities on all items + foreach (var prop in allProps) + { + // Get related entities + string propertyName = prop.Name; + Type propertyType = prop.PropertyType; + IEnumerable relatedEntities = context.GetRelatedEntities(entities, + prop.DeclaringType, propertyName, propertyType, onError); - // Continue if there are no related entities - if (!relatedEntities.Any()) continue; + // Continue if there are no related entities + if (!relatedEntities.Any()) continue; - // ObjectVisitationHelper serves here as an identity cache - relatedEntities = relatedEntities.Select(e => visitationHelper.FindVisited(e) ?? e); + // ObjectVisitationHelper serves here as an identity cache + relatedEntities = relatedEntities.Select(e => visitationHelper.FindVisited(e) ?? e); - // Set related entities - context.SetRelatedEntities(entities, relatedEntities, prop, - prop.DeclaringType, propertyName, propertyType); + // Set related entities + context.SetRelatedEntities(entities, relatedEntities, prop, + prop.DeclaringType, propertyName, propertyType); + } } // Recursively populate related entities on ref and child properties @@ -398,14 +420,15 @@ where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) bool loadAllRelated = loadAll || item.TrackingState == TrackingState.Added || (parent != null && parent.TrackingState == TrackingState.Added); - context.LoadRelatedEntitiesOnProperties(item, visitationHelper, loadAllRelated); + context.LoadRelatedEntitiesOnProperties(item, visitationHelper, loadAllRelated, onLoading, onError); } } #if EF_6 private static async Task LoadRelatedEntitiesAsync(this DbContext context, IEnumerable items, ITrackable parent, ObjectVisitationHelper visitationHelper, - CancellationToken cancellationToken, bool loadAll) + CancellationToken cancellationToken, bool loadAll, + Func, Task> onLoading, Func, Task> onError) { // Return if no items if (items == null) return; @@ -417,49 +440,54 @@ private static async Task LoadRelatedEntitiesAsync(this DbContext context, var entities = selectedItems.Cast(). Where(i => !IsComplexType(context, i.GetType()) && !visitationHelper.IsVisited(i)); - // Collection 'items' can contain entities of different types (due to inheritance) - // We collect a superset of all properties of all items of type ITrackable - var allProps = (from entity in entities - from prop in entity.GetType().GetProperties() - where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) - select prop).Distinct(); - - // Populate related entities on all items - foreach (var prop in allProps) + // Pre-loading hook + if (onLoading == null || await onLoading(entities.OfType())) { - // Get related entities - string propertyName = prop.Name; - Type propertyType = prop.PropertyType; - IEnumerable relatedEntities = await context.GetRelatedEntitiesAsync(entities, - prop.DeclaringType, propertyName, propertyType, cancellationToken); - - // Continue if there are no related entities - if (!relatedEntities.Any()) continue; + // Collection 'items' can contain entities of different types (due to inheritance) + // We collect a superset of all properties of all items of type ITrackable + var allProps = (from entity in entities + from prop in entity.GetType().GetProperties() + where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) + select prop).Distinct(); + + // Populate related entities on all items + foreach (var prop in allProps) + { + // Get related entities + string propertyName = prop.Name; + Type propertyType = prop.PropertyType; + IEnumerable relatedEntities = await context.GetRelatedEntitiesAsync(entities, + prop.DeclaringType, propertyName, propertyType, cancellationToken, onError); - // ObjectVisitationHelper serves here as an identity cache - relatedEntities = relatedEntities.Select(e => visitationHelper.FindVisited(e) ?? e); + // Continue if there are no related entities + if (!relatedEntities.Any()) continue; - // Set related entities - context.SetRelatedEntities(entities, relatedEntities, prop, - prop.DeclaringType, propertyName, propertyType); - } + // ObjectVisitationHelper serves here as an identity cache + relatedEntities = relatedEntities.Select(e => visitationHelper.FindVisited(e) ?? e); - // Recursively populate related entities on ref and child properties - foreach (var item in items) - { - // Avoid loading related entities of complext types, and avoid endless recursion - if (IsComplexType(context, item.GetType()) || !visitationHelper.TryVisit(item)) continue; + // Set related entities + context.SetRelatedEntities(entities, relatedEntities, prop, + prop.DeclaringType, propertyName, propertyType); + } - bool loadAllRelated = loadAll - || item.TrackingState == TrackingState.Added - || (parent != null && parent.TrackingState == TrackingState.Added); - await context.LoadRelatedEntitiesOnPropertiesAsync(item, visitationHelper, - cancellationToken, loadAllRelated); + // Recursively populate related entities on ref and child properties + foreach (var item in items) + { + // Avoid loading related entities of complext types, and avoid endless recursion + if (IsComplexType(context, item.GetType()) || !visitationHelper.TryVisit(item)) continue; + + bool loadAllRelated = loadAll + || item.TrackingState == TrackingState.Added + || (parent != null && parent.TrackingState == TrackingState.Added); + await context.LoadRelatedEntitiesOnPropertiesAsync(item, visitationHelper, + cancellationToken, loadAllRelated, onLoading, onError); + } } } private async static Task LoadRelatedEntitiesOnPropertiesAsync(this DbContext context, - ITrackable item, ObjectVisitationHelper visitationHelper, CancellationToken cancellationToken, bool loadAll) + ITrackable item, ObjectVisitationHelper visitationHelper, CancellationToken cancellationToken, bool loadAll, + Func, Task> onLoading, Func, Task> onError) { // Recursively load related entities foreach (var navProp in item.GetNavigationProperties()) @@ -468,21 +496,22 @@ private async static Task LoadRelatedEntitiesOnPropertiesAsync(this DbContext co foreach (var refProp in navProp.AsReferenceProperty()) { await context.LoadRelatedEntitiesAsync(new[] { refProp.EntityReference }, item, visitationHelper, - cancellationToken, loadAll); + cancellationToken, loadAll, onLoading, onError); } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { await context.LoadRelatedEntitiesAsync(colProp.EntityCollection, item, visitationHelper, - cancellationToken, loadAll); + cancellationToken, loadAll, onLoading, onError); } } } #endif private static void LoadRelatedEntitiesOnProperties(this DbContext context, - ITrackable item, ObjectVisitationHelper visitationHelper, bool loadAll) + ITrackable item, ObjectVisitationHelper visitationHelper, bool loadAll, + Func, bool> onLoading, Func, bool> onError) { // Recursively load related entities foreach (var navProp in item.GetNavigationProperties()) @@ -490,13 +519,13 @@ private static void LoadRelatedEntitiesOnProperties(this DbContext context, // Apply changes to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { - context.LoadRelatedEntities(new[] { refProp.EntityReference }, item, visitationHelper, loadAll); + context.LoadRelatedEntities(new[] { refProp.EntityReference }, item, visitationHelper, loadAll, onLoading, onError); } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { - context.LoadRelatedEntities(colProp.EntityCollection, item, visitationHelper, loadAll); + context.LoadRelatedEntities(colProp.EntityCollection, item, visitationHelper, loadAll, onLoading, onError); } } } @@ -711,7 +740,8 @@ private static RelationshipType GetRelationshipType(this DbContext dbContext, Ty if (navProp == null) throw new ArgumentException("Getting navigation property failed.", "propertyName"); - if (navProp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One + if ((navProp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One + || navProp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne) && (navProp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne || navProp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One)) return RelationshipType.OneToOne; @@ -786,30 +816,67 @@ private static void SetEntityState(DbContext context, #region LoadRelatedEntities Helpers private static List GetRelatedEntities(this DbContext context, - IEnumerable items, Type entityType, string propertyName, Type propertyType) + IEnumerable items, Type entityType, string propertyName, + Type propertyType, Func, bool> onError) { // Get entity sql + List entities = new List(); string entitySql = context.GetRelatedEntitiesSql(items, entityType, propertyName, propertyType); if (string.IsNullOrWhiteSpace(entitySql)) return new List(); - // Get related entities - List entities = context.ExecuteQueryEntitySql(entitySql); - return entities; + try + { + // Get related entities + entities = context.ExecuteQueryEntitySql(entitySql); + return entities; + } + catch (Exception ex) + { + if (onError != null) + { + if (onError(ex, items.OfType())) + return entities; + throw; + } + throw; + } } #if EF_6 private static async Task> GetRelatedEntitiesAsync(this DbContext context, IEnumerable items, Type entityType, string propertyName, Type propertyType, - CancellationToken cancellationToken) + CancellationToken cancellationToken, Func, Task> onError) { // Get entity sql + List entities = new List(); string entitySql = context.GetRelatedEntitiesSql(items, entityType, propertyName, propertyType); if (string.IsNullOrWhiteSpace(entitySql)) return new List(); - // Get related entities - List entities = await context.ExecuteQueryEntitySqlAsync(entitySql, cancellationToken); + ExceptionDispatchInfo capturedException = null; + + try + { + // Get related entities + entities = await context.ExecuteQueryEntitySqlAsync(entitySql, cancellationToken); + } + catch (Exception ex) + { + capturedException = ExceptionDispatchInfo.Capture(ex); + } + + if (capturedException != null) + { + if (onError != null) + { + if (await onError(capturedException.SourceException, items.OfType())) + return entities; + capturedException.Throw(); + } + capturedException.Throw(); + } return entities; - } + } + #endif private static string GetRelatedEntitiesSql(this DbContext context, diff --git a/build-test.cmd b/build-test.cmd index 2724a04f..2bb980fe 100644 --- a/build-test.cmd +++ b/build-test.cmd @@ -1,4 +1,4 @@ -set PackageVersion=2.5.6.1 +set PackageVersion=2.5.7 rem set Configuration=Debug rem build.cmd -debug build.cmd \ No newline at end of file diff --git a/build.cmd b/build.cmd index 71683dbc..4fd8cd66 100644 --- a/build.cmd +++ b/build.cmd @@ -37,8 +37,8 @@ call Build\Scripts\TrackableEntities.Patterns.EF.6.cmd %debug% if not "%errorlevel%"=="0" goto failure REM TrackableEntities.CodeTemplates: -call Build\Scripts\TrackableEntities.CodeTemplates.cmd %debug% -if not "%errorlevel%"=="0" goto failure +rem call Build\Scripts\TrackableEntities.CodeTemplates.cmd %debug% +rem if not "%errorlevel%"=="0" goto failure :success if "%debug%"=="0" exit 0