diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs
index 781956b..c58758c 100644
--- a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using CommunityToolkit.Datasync.Server;
-using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using System.Text.Json.Serialization;
diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs
new file mode 100644
index 0000000..239209c
--- /dev/null
+++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Client.Offline;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace CommunityToolkit.Datasync.Client.Test.Offline;
+
+///
+/// Tests for: https://github.com/CommunityToolkit/Datasync/issues/217
+/// See https://github.com/david1995/CommunityToolKit.Datasync-DynamicProxiesRepro/blob/main/ConsoleApp1/Program.cs
+/// See https://github.com/CommunityToolkit/Datasync/issues/211
+///
+[ExcludeFromCodeCoverage]
+public class DynamicProxies_Tests : IDisposable
+{
+ private readonly string temporaryDbPath;
+ private readonly string dataSource;
+ private bool _disposedValue;
+
+ public DynamicProxies_Tests()
+ {
+ this.temporaryDbPath = $"{Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())}.sqlite";
+ this.dataSource = $"Data Source={this.temporaryDbPath};Foreign Keys=False";
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!this._disposedValue)
+ {
+ if (disposing)
+ {
+ // Really release the DB
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ // If the file exists, it should be able to be deleted now.
+ if (File.Exists(this.temporaryDbPath))
+ {
+ File.Delete(this.temporaryDbPath);
+ }
+ }
+
+ this._disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public async Task OfflineDbContext_Queue_SupportsDynamicProxies()
+ {
+ SqliteConnection connection = new(this.dataSource);
+ connection.Open();
+
+ try
+ {
+ DbContextOptions dbContextOptions = new DbContextOptionsBuilder()
+ .UseSqlite(connection)
+ .UseLazyLoadingProxies()
+ .Options;
+
+ string key = Guid.CreateVersion7().ToString();
+ await using (DynamicProxiesTestContext context = new(dbContextOptions))
+ {
+ context.Database.EnsureCreated();
+ await context.DynamicProxiesEntities1.AddAsync(new DynamicProxiesEntity1
+ {
+ Id = key,
+ Name = $"Test {DateTime.Now}",
+ LocalNotes = "These notes should not be serialized into DatasyncOperationsQueue",
+ RelatedEntity = new() { Id = Guid.NewGuid().ToString() }
+ });
+ await context.SaveChangesAsync();
+ }
+
+ await using (DynamicProxiesTestContext context = new(dbContextOptions))
+ {
+ DatasyncOperation operationAfterInsert = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key);
+ operationAfterInsert.EntityType.Should().EndWith("DynamicProxiesEntity1");
+ operationAfterInsert.Version.Should().Be(0);
+
+ // The LocalNotes should not be included
+ operationAfterInsert.Item.Should().NotContain("\"localNotes\":");
+
+ // Update the entity within the DbContext
+ DynamicProxiesEntity1 entity = await context.DynamicProxiesEntities1.FirstAsync(e => e.Id == key);
+ string updatedName = $"Updated name {DateTime.Now}";
+ entity.Name = updatedName;
+ await context.SaveChangesAsync();
+
+ // There should be 1 operation.
+ int operationsWithItemId = await context.DatasyncOperationsQueue.CountAsync(o => o.ItemId == key);
+ operationsWithItemId.Should().Be(1);
+
+ // Here is the operation after edit.
+ DatasyncOperation operationAfterEdit = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key);
+ operationAfterEdit.EntityType.Should().EndWith("DynamicProxiesEntity1");
+ operationAfterEdit.Version.Should().Be(1);
+ operationAfterEdit.Item.Should().Contain($"\"name\":\"{updatedName}\"");
+
+ // The LocalNotes should not be included
+ operationAfterEdit.Item.Should().NotContain("\"localNotes\":");
+ }
+ }
+ finally
+ {
+ connection.Close();
+ connection.Dispose();
+ SqliteConnection.ClearAllPools();
+ }
+ }
+}
+
+public class DynamicProxiesTestContext(DbContextOptions options) : OfflineDbContext(options)
+{
+ public virtual DbSet DynamicProxiesEntities1 { get; set; }
+
+ public virtual DbSet DynamicProxiesEntities2 { get; set; }
+
+ protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.Entity(typeof(DynamicProxiesEntity1), _ => { });
+ optionsBuilder.Entity(typeof(DynamicProxiesEntity2), _ => { });
+ }
+}
+
+public abstract class DatasyncBase
+{
+ [Key, StringLength(200)]
+ public string Id { get; set; } = null!;
+
+ public DateTimeOffset? UpdatedAt { get; set; }
+
+ public string Version { get; set; }
+
+ public bool Deleted { get; set; }
+}
+
+public class DynamicProxiesEntity1 : DatasyncBase
+{
+ [StringLength(255)]
+ public string Name { get; set; }
+
+ // this should not be synchronized
+ [JsonIgnore]
+ [StringLength(255)]
+ public string LocalNotes { get; set; }
+
+ [StringLength(200)]
+ public string RelatedEntityId { get; set; }
+
+ // this property should also not be serialized
+ [JsonIgnore]
+ public virtual DynamicProxiesEntity2 RelatedEntity { get; set; }
+}
+
+public class DynamicProxiesEntity2 : DatasyncBase;
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs
index 74d0c6f..9815f13 100644
--- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs
@@ -5,7 +5,6 @@
using CommunityToolkit.Datasync.Server.Abstractions.Json;
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
using CommunityToolkit.Datasync.TestCommon;
-using CommunityToolkit.Datasync.TestCommon.Databases;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
using System.Collections.ObjectModel;
@@ -42,6 +41,7 @@ protected virtual void Dispose(bool disposing)
}
override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString);
+
protected override async Task GetEntityAsync(string id)
{
try
@@ -109,7 +109,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection()
- {
+ {
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
@@ -121,7 +121,7 @@ public async Task InitializeAsync()
}
}
});
-
+
foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType())
{
movie.UpdatedAt = DateTimeOffset.UtcNow;
@@ -134,7 +134,6 @@ public async Task InitializeAsync()
this._client,
new CosmosSingleTableOptions("Movies", "Movies")
);
-
}
}
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs
new file mode 100644
index 0000000..e834fc0
--- /dev/null
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Common.Test;
+using CommunityToolkit.Datasync.Server.CosmosDb;
+using CommunityToolkit.Datasync.TestCommon;
+using FluentAssertions;
+
+namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test;
+
+[ExcludeFromCodeCoverage]
+public class CosmosTableData_Tests
+{
+ [Theory, ClassData(typeof(ITableData_TestData))]
+ public void CCosmosTableData_Equals(ITableData a, ITableData b, bool expected)
+ {
+ CCosmosTableData entity_a = a.ToTableEntity();
+ CCosmosTableData entity_b = b.ToTableEntity();
+
+ entity_a.Equals(entity_b).Should().Be(expected);
+ entity_b.Equals(entity_a).Should().Be(expected);
+
+ entity_a.Equals(null).Should().BeFalse();
+ entity_b.Equals(null).Should().BeFalse();
+ }
+
+ [Fact]
+ public void CCosmosTableData_MetadataRoundtrips()
+ {
+ DateTimeOffset testTime = DateTimeOffset.Now;
+
+ CCosmosTableData sut1 = new() { Id = "t1", Deleted = false, UpdatedAt = testTime, Version = [0x61, 0x62, 0x63, 0x64, 0x65] };
+ sut1.Version.Should().BeEquivalentTo("abcde"u8.ToArray());
+ sut1.UpdatedAt.Should().Be(testTime);
+ }
+}
+
+[ExcludeFromCodeCoverage]
+public class CCosmosTableData : CosmosTableData
+{
+}
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs
index 76d7d06..8bdb67b 100644
--- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs
@@ -3,11 +3,6 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Datasync.TestCommon.Models;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
public class CosmosDbMovie : CosmosTableData, IMovie
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs
index 00827d9..5e65eef 100644
--- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs
@@ -4,20 +4,12 @@
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
using Microsoft.Azure.Cosmos;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Options;
-public class PackedKeyOptions : CosmosSingleTableOptions
+public class PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true)
+ : CosmosSingleTableOptions(databaseId, containerId, shouldUpdateTimestamp)
{
- public PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
- {
- }
-
public override Func IdGenerator => (entity) => $"{Guid.NewGuid()}:{entity.Year}";
public override string GetPartitionKey(CosmosDbMovie entity, out PartitionKey partitionKey)
{
@@ -35,7 +27,9 @@ public override string ParsePartitionKey(string id, out PartitionKey partitionKe
}
if (!int.TryParse(parts[1], out int year))
+ {
throw new ArgumentException("Invalid ID Part");
+ }
partitionKey = new PartitionKey(year);
return id;
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs
index c71abb4..3ee6a60 100644
--- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs
@@ -6,7 +6,6 @@
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Options;
using CommunityToolkit.Datasync.TestCommon;
-using CommunityToolkit.Datasync.TestCommon.Databases;
using FluentAssertions;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
@@ -57,7 +56,9 @@ protected override async Task GetEntityAsync(string id)
}
if (!int.TryParse(parts[1], out int year))
+ {
throw new ArgumentException("Invalid ID Part");
+ }
return await this._container.ReadItemAsync(id, new PartitionKey(year));
}
@@ -122,7 +123,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection()
- {
+ {
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
@@ -134,7 +135,7 @@ public async Task InitializeAsync()
}
}
});
-
+
foreach (CosmosDbMovie movie in TestData.Movies.OfType())
{
movie.Id = $"{Guid.NewGuid()}:{movie.Year}";
@@ -148,7 +149,6 @@ public async Task InitializeAsync()
this._client,
new PackedKeyOptions("Movies", "Movies")
);
-
}
}
@@ -180,6 +180,7 @@ public async Task ReadAsync_Throws_OnMalformedId(string id)
(await act.Should().ThrowAsync()).WithStatusCode(400);
}
+
[SkippableTheory]
[InlineData("BadId")]
[InlineData("12345-12345")]
@@ -193,5 +194,4 @@ public async Task DeleteAsync_Throws_OnMalformedIds(string id)
(await act.Should().ThrowAsync()).WithStatusCode(400);
(await GetEntityCountAsync()).Should().Be(TestData.Movies.Count());
}
-
}