diff --git a/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs b/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs index 1c4fab50..11056072 100644 --- a/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs +++ b/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs @@ -114,10 +114,10 @@ internal Dictionary GetEntityMap(OfflineDbContext context) /// A to observe. /// The operation entity or null if one does not exist. /// Thrown if the entity ID of the provided entity is invalid. - internal async ValueTask GetExistingOperationAsync(object entity, CancellationToken cancellationToken = default) + internal async ValueTask GetExistingOperationAsync(EntityEntry entityEntry, CancellationToken cancellationToken = default) { - Type entityType = entity.GetType(); - EntityMetadata metadata = EntityResolver.GetEntityMetadata(entity, entityType); + Type entityType = entityEntry.Metadata.ClrType; + EntityMetadata metadata = EntityResolver.GetEntityMetadata(entityEntry.Entity, entityType); if (!EntityResolver.EntityIdIsValid(metadata.Id)) { throw new DatasyncException($"Entity ID for type {entityType.FullName} is invalid."); @@ -143,7 +143,7 @@ internal Task GetLastSequenceIdAsync(CancellationToken cancellationToken = /// The operation definition. internal DatasyncOperation GetOperationForChangedEntity(EntityEntry entry) { - Type entityType = entry.Entity.GetType(); + Type entityType = entry.Metadata.ClrType; EntityMetadata metadata = EntityResolver.GetEntityMetadata(entry.Entity, entityType); if (!EntityResolver.EntityIdIsValid(metadata.Id)) { @@ -432,7 +432,7 @@ public async Task UpdateOperationsQueueAsync(CancellationToken cancellationToken foreach (EntityEntry entry in entitiesInScope) { DatasyncOperation newOperation = GetOperationForChangedEntity(entry); - DatasyncOperation? existingOperation = await GetExistingOperationAsync(entry.Entity, cancellationToken).ConfigureAwait(false); + DatasyncOperation? existingOperation = await GetExistingOperationAsync(entry, cancellationToken).ConfigureAwait(false); if (existingOperation is null) { newOperation.Sequence = Interlocked.Increment(ref sequenceId); diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Helpers/BaseTest.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Helpers/BaseTest.cs index 1a794f87..e5ac057f 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Helpers/BaseTest.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Helpers/BaseTest.cs @@ -25,8 +25,22 @@ public abstract class BaseTest /// protected static TestDbContext CreateContext(Action> configureOptions = null) { - SqliteConnection connection = new("Data Source=:memory:"); - connection.Open(); + SqliteConnection connection = CreateAndOpenConnection(); + DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder() + .UseSqlite(connection); + configureOptions?.Invoke(optionsBuilder); + TestDbContext context = new(optionsBuilder.Options) { Connection = connection }; + + // Ensure the database is created. + context.Database.EnsureCreated(); + return context; + } + + /// + /// Creates a version of the TestDbContext backed by the specified SQLite connection. + /// + protected static TestDbContext CreateContext(SqliteConnection connection, Action> configureOptions = null) + { DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder() .UseSqlite(connection); configureOptions?.Invoke(optionsBuilder); @@ -37,6 +51,16 @@ protected static TestDbContext CreateContext(Action + /// Creates and opens an in-memory SQLite database connection. + /// + protected static SqliteConnection CreateAndOpenConnection() + { + SqliteConnection connection = new("Data Source=:memory:"); + connection.Open(); + return connection; + } + /// /// Creates a response message based on code and content. /// diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs index 5aa9fb39..acb41285 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Datasync.TestCommon.Databases; using Microsoft.EntityFrameworkCore; using System.Net; +using Microsoft.Data.Sqlite; using TestData = CommunityToolkit.Datasync.TestCommon.TestData; namespace CommunityToolkit.Datasync.Client.Test.Offline; @@ -39,7 +40,7 @@ public void GetEntityMap_Works() public async Task GetExistingOperationAsync_InvalidId_Throws() { ClientMovie movie = new() { Id = "###" }; - Func act = async () => _ = await queueManager.GetExistingOperationAsync(movie); + Func act = async () => _ = await queueManager.GetExistingOperationAsync(context.Entry(movie)); await act.Should().ThrowAsync(); } #endregion @@ -482,5 +483,32 @@ public async Task LLP_PushAsync_Replacement_Works() llpContext.DatasyncOperationsQueue.Should().BeEmpty(); } + + [Fact] + public async Task LLP_ModifyAfterInsertInNewContext_NoPush_ShouldUpdateOperationsQueue() + { + await using SqliteConnection connection = CreateAndOpenConnection(); + string id = Guid.NewGuid().ToString("N"); + await using (TestDbContext llpContext = CreateContext(connection, x => x.UseLazyLoadingProxies())) + { + ClientMovie clientMovie = new(TestData.Movies.MovieList[0].Title) { Id = id }; + llpContext.Movies.Add(clientMovie); + llpContext.SaveChanges(); + } + + await using TestDbContext newLlpContext = CreateContext(connection, x => x.UseLazyLoadingProxies()); + + ClientMovie storedClientMovie = newLlpContext.Movies.First(m => m.Id == id); + + // ensure that it is a lazy loading proxy and not exactly a ClientMovie + storedClientMovie.GetType().Should().NotBe(typeof(ClientMovie)) + .And.Subject.Namespace.Should().Be("Castle.Proxies"); + + storedClientMovie.Title = TestData.Movies.MovieList[1].Title; + newLlpContext.SaveChanges(); + + newLlpContext.DatasyncOperationsQueue.Should().ContainSingle(op => op.ItemId == id) + .Which.EntityType.Should().NotContain("Castle.Proxies"); + } #endregion }