Skip to content

Commit

Permalink
(#211) Client: Fix wrong EntityType in OperationsQueue when using Laz…
Browse files Browse the repository at this point in the history
…y Loading Proxies (#212)

* add test

* replace GetType() calls with Metadata CLR type

* impove test

---------

Co-authored-by: Adrian Hall <[email protected]>
  • Loading branch information
david1995 and adrianhall authored Jan 21, 2025
1 parent c860e96 commit 49bd9d1
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ internal Dictionary<string, Type> GetEntityMap(OfflineDbContext context)
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>The operation entity or null if one does not exist.</returns>
/// <exception cref="DatasyncException">Thrown if the entity ID of the provided entity is invalid.</exception>
internal async ValueTask<DatasyncOperation?> GetExistingOperationAsync(object entity, CancellationToken cancellationToken = default)
internal async ValueTask<DatasyncOperation?> GetExistingOperationAsync(EntityEntry entityEntry, CancellationToken cancellationToken = default)

Check warning on line 117 in src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'entityEntry' has no matching param tag in the XML comment for 'OperationsQueueManager.GetExistingOperationAsync(EntityEntry, CancellationToken)' (but other parameters do)

Check warning on line 117 in src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'entityEntry' has no matching param tag in the XML comment for 'OperationsQueueManager.GetExistingOperationAsync(EntityEntry, CancellationToken)' (but other parameters do)
{
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.");
Expand All @@ -143,7 +143,7 @@ internal Task<long> GetLastSequenceIdAsync(CancellationToken cancellationToken =
/// <returns>The operation definition.</returns>
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))
{
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,22 @@ public abstract class BaseTest
/// </summary>
protected static TestDbContext CreateContext(Action<DbContextOptionsBuilder<TestDbContext>> configureOptions = null)
{
SqliteConnection connection = new("Data Source=:memory:");
connection.Open();
SqliteConnection connection = CreateAndOpenConnection();
DbContextOptionsBuilder<TestDbContext> optionsBuilder = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlite(connection);
configureOptions?.Invoke(optionsBuilder);
TestDbContext context = new(optionsBuilder.Options) { Connection = connection };

// Ensure the database is created.
context.Database.EnsureCreated();
return context;
}

/// <summary>
/// Creates a version of the TestDbContext backed by the specified SQLite connection.
/// </summary>
protected static TestDbContext CreateContext(SqliteConnection connection, Action<DbContextOptionsBuilder<TestDbContext>> configureOptions = null)
{
DbContextOptionsBuilder<TestDbContext> optionsBuilder = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlite(connection);
configureOptions?.Invoke(optionsBuilder);
Expand All @@ -37,6 +51,16 @@ protected static TestDbContext CreateContext(Action<DbContextOptionsBuilder<Test
return context;
}

/// <summary>
/// Creates and opens an in-memory SQLite database connection.
/// </summary>
protected static SqliteConnection CreateAndOpenConnection()
{
SqliteConnection connection = new("Data Source=:memory:");
connection.Open();
return connection;
}

/// <summary>
/// Creates a response message based on code and content.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,7 +40,7 @@ public void GetEntityMap_Works()
public async Task GetExistingOperationAsync_InvalidId_Throws()
{
ClientMovie movie = new() { Id = "###" };
Func<Task> act = async () => _ = await queueManager.GetExistingOperationAsync(movie);
Func<Task> act = async () => _ = await queueManager.GetExistingOperationAsync(context.Entry(movie));

Check warning on line 43 in tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs

View workflow job for this annotation

GitHub Actions / build

Add 'this' or 'Me' qualification (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009)

Check warning on line 43 in tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs

View workflow job for this annotation

GitHub Actions / build

Add 'this' or 'Me' qualification (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009)
await act.Should().ThrowAsync<DatasyncException>();
}
#endregion
Expand Down Expand Up @@ -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
}

0 comments on commit 49bd9d1

Please sign in to comment.