Skip to content

Commit 0595fd8

Browse files
authored
(#217) Added dynamic proxies tests, plus some Cosmos test fixes. (#303)
1 parent 9a0f507 commit 0595fd8

File tree

7 files changed

+220
-26
lines changed

7 files changed

+220
-26
lines changed

src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.Datasync.Server;
6-
using System.ComponentModel.DataAnnotations.Schema;
75
using System.Text;
86
using System.Text.Json.Serialization;
97

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Datasync.Client.Offline;
6+
using Microsoft.Data.Sqlite;
7+
using Microsoft.EntityFrameworkCore;
8+
using System.ComponentModel.DataAnnotations;
9+
using System.Text.Json.Serialization;
10+
11+
namespace CommunityToolkit.Datasync.Client.Test.Offline;
12+
13+
/// <summary>
14+
/// Tests for: https://github.com/CommunityToolkit/Datasync/issues/217
15+
/// See https://github.com/david1995/CommunityToolKit.Datasync-DynamicProxiesRepro/blob/main/ConsoleApp1/Program.cs
16+
/// See https://github.com/CommunityToolkit/Datasync/issues/211
17+
/// </summary>
18+
[ExcludeFromCodeCoverage]
19+
public class DynamicProxies_Tests : IDisposable
20+
{
21+
private readonly string temporaryDbPath;
22+
private readonly string dataSource;
23+
private bool _disposedValue;
24+
25+
public DynamicProxies_Tests()
26+
{
27+
this.temporaryDbPath = $"{Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())}.sqlite";
28+
this.dataSource = $"Data Source={this.temporaryDbPath};Foreign Keys=False";
29+
}
30+
31+
protected virtual void Dispose(bool disposing)
32+
{
33+
if (!this._disposedValue)
34+
{
35+
if (disposing)
36+
{
37+
// Really release the DB
38+
GC.Collect();
39+
GC.WaitForPendingFinalizers();
40+
41+
// If the file exists, it should be able to be deleted now.
42+
if (File.Exists(this.temporaryDbPath))
43+
{
44+
File.Delete(this.temporaryDbPath);
45+
}
46+
}
47+
48+
this._disposedValue = true;
49+
}
50+
}
51+
52+
public void Dispose()
53+
{
54+
Dispose(disposing: true);
55+
GC.SuppressFinalize(this);
56+
}
57+
58+
[Fact]
59+
public async Task OfflineDbContext_Queue_SupportsDynamicProxies()
60+
{
61+
SqliteConnection connection = new(this.dataSource);
62+
connection.Open();
63+
64+
try
65+
{
66+
DbContextOptions<DynamicProxiesTestContext> dbContextOptions = new DbContextOptionsBuilder<DynamicProxiesTestContext>()
67+
.UseSqlite(connection)
68+
.UseLazyLoadingProxies()
69+
.Options;
70+
71+
string key = Guid.CreateVersion7().ToString();
72+
await using (DynamicProxiesTestContext context = new(dbContextOptions))
73+
{
74+
context.Database.EnsureCreated();
75+
await context.DynamicProxiesEntities1.AddAsync(new DynamicProxiesEntity1
76+
{
77+
Id = key,
78+
Name = $"Test {DateTime.Now}",
79+
LocalNotes = "These notes should not be serialized into DatasyncOperationsQueue",
80+
RelatedEntity = new() { Id = Guid.NewGuid().ToString() }
81+
});
82+
await context.SaveChangesAsync();
83+
}
84+
85+
await using (DynamicProxiesTestContext context = new(dbContextOptions))
86+
{
87+
DatasyncOperation operationAfterInsert = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key);
88+
operationAfterInsert.EntityType.Should().EndWith("DynamicProxiesEntity1");
89+
operationAfterInsert.Version.Should().Be(0);
90+
91+
// The LocalNotes should not be included
92+
operationAfterInsert.Item.Should().NotContain("\"localNotes\":");
93+
94+
// Update the entity within the DbContext
95+
DynamicProxiesEntity1 entity = await context.DynamicProxiesEntities1.FirstAsync(e => e.Id == key);
96+
string updatedName = $"Updated name {DateTime.Now}";
97+
entity.Name = updatedName;
98+
await context.SaveChangesAsync();
99+
100+
// There should be 1 operation.
101+
int operationsWithItemId = await context.DatasyncOperationsQueue.CountAsync(o => o.ItemId == key);
102+
operationsWithItemId.Should().Be(1);
103+
104+
// Here is the operation after edit.
105+
DatasyncOperation operationAfterEdit = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key);
106+
operationAfterEdit.EntityType.Should().EndWith("DynamicProxiesEntity1");
107+
operationAfterEdit.Version.Should().Be(1);
108+
operationAfterEdit.Item.Should().Contain($"\"name\":\"{updatedName}\"");
109+
110+
// The LocalNotes should not be included
111+
operationAfterEdit.Item.Should().NotContain("\"localNotes\":");
112+
}
113+
}
114+
finally
115+
{
116+
connection.Close();
117+
connection.Dispose();
118+
SqliteConnection.ClearAllPools();
119+
}
120+
}
121+
}
122+
123+
public class DynamicProxiesTestContext(DbContextOptions options) : OfflineDbContext(options)
124+
{
125+
public virtual DbSet<DynamicProxiesEntity1> DynamicProxiesEntities1 { get; set; }
126+
127+
public virtual DbSet<DynamicProxiesEntity2> DynamicProxiesEntities2 { get; set; }
128+
129+
protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
130+
{
131+
optionsBuilder.Entity(typeof(DynamicProxiesEntity1), _ => { });
132+
optionsBuilder.Entity(typeof(DynamicProxiesEntity2), _ => { });
133+
}
134+
}
135+
136+
public abstract class DatasyncBase
137+
{
138+
[Key, StringLength(200)]
139+
public string Id { get; set; } = null!;
140+
141+
public DateTimeOffset? UpdatedAt { get; set; }
142+
143+
public string Version { get; set; }
144+
145+
public bool Deleted { get; set; }
146+
}
147+
148+
public class DynamicProxiesEntity1 : DatasyncBase
149+
{
150+
[StringLength(255)]
151+
public string Name { get; set; }
152+
153+
// this should not be synchronized
154+
[JsonIgnore]
155+
[StringLength(255)]
156+
public string LocalNotes { get; set; }
157+
158+
[StringLength(200)]
159+
public string RelatedEntityId { get; set; }
160+
161+
// this property should also not be serialized
162+
[JsonIgnore]
163+
public virtual DynamicProxiesEntity2 RelatedEntity { get; set; }
164+
}
165+
166+
public class DynamicProxiesEntity2 : DatasyncBase;

tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using CommunityToolkit.Datasync.Server.Abstractions.Json;
66
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
77
using CommunityToolkit.Datasync.TestCommon;
8-
using CommunityToolkit.Datasync.TestCommon.Databases;
98
using Microsoft.Azure.Cosmos;
109
using Microsoft.Azure.Cosmos.Linq;
1110
using System.Collections.ObjectModel;
@@ -42,6 +41,7 @@ protected virtual void Dispose(bool disposing)
4241
}
4342

4443
override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString);
44+
4545
protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
4646
{
4747
try
@@ -109,7 +109,7 @@ public async Task InitializeAsync()
109109
CompositeIndexes =
110110
{
111111
new Collection<CompositePath>()
112-
{
112+
{
113113
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
114114
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
115115
},
@@ -121,7 +121,7 @@ public async Task InitializeAsync()
121121
}
122122
}
123123
});
124-
124+
125125
foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType<CosmosDbMovie>())
126126
{
127127
movie.UpdatedAt = DateTimeOffset.UtcNow;
@@ -134,7 +134,6 @@ public async Task InitializeAsync()
134134
this._client,
135135
new CosmosSingleTableOptions<CosmosDbMovie>("Movies", "Movies")
136136
);
137-
138137
}
139138
}
140139

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Datasync.Common.Test;
6+
using CommunityToolkit.Datasync.Server.CosmosDb;
7+
using CommunityToolkit.Datasync.TestCommon;
8+
using FluentAssertions;
9+
10+
namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test;
11+
12+
[ExcludeFromCodeCoverage]
13+
public class CosmosTableData_Tests
14+
{
15+
[Theory, ClassData(typeof(ITableData_TestData))]
16+
public void CCosmosTableData_Equals(ITableData a, ITableData b, bool expected)
17+
{
18+
CCosmosTableData entity_a = a.ToTableEntity<CCosmosTableData>();
19+
CCosmosTableData entity_b = b.ToTableEntity<CCosmosTableData>();
20+
21+
entity_a.Equals(entity_b).Should().Be(expected);
22+
entity_b.Equals(entity_a).Should().Be(expected);
23+
24+
entity_a.Equals(null).Should().BeFalse();
25+
entity_b.Equals(null).Should().BeFalse();
26+
}
27+
28+
[Fact]
29+
public void CCosmosTableData_MetadataRoundtrips()
30+
{
31+
DateTimeOffset testTime = DateTimeOffset.Now;
32+
33+
CCosmosTableData sut1 = new() { Id = "t1", Deleted = false, UpdatedAt = testTime, Version = [0x61, 0x62, 0x63, 0x64, 0x65] };
34+
sut1.Version.Should().BeEquivalentTo("abcde"u8.ToArray());
35+
sut1.UpdatedAt.Should().Be(testTime);
36+
}
37+
}
38+
39+
[ExcludeFromCodeCoverage]
40+
public class CCosmosTableData : CosmosTableData
41+
{
42+
}

tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@
33
// See the LICENSE file in the project root for more information.
44

55
using CommunityToolkit.Datasync.TestCommon.Models;
6-
using System;
7-
using System.Collections.Generic;
8-
using System.Linq;
9-
using System.Text;
10-
using System.Threading.Tasks;
116

127
namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
138
public class CosmosDbMovie : CosmosTableData, IMovie

tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,12 @@
44

55
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
66
using Microsoft.Azure.Cosmos;
7-
using System;
8-
using System.Collections.Generic;
9-
using System.Linq;
10-
using System.Text;
11-
using System.Threading.Tasks;
127

138
namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Options;
149

15-
public class PackedKeyOptions : CosmosSingleTableOptions<CosmosDbMovie>
10+
public class PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true)
11+
: CosmosSingleTableOptions<CosmosDbMovie>(databaseId, containerId, shouldUpdateTimestamp)
1612
{
17-
public PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
18-
{
19-
}
20-
2113
public override Func<CosmosDbMovie, string> IdGenerator => (entity) => $"{Guid.NewGuid()}:{entity.Year}";
2214
public override string GetPartitionKey(CosmosDbMovie entity, out PartitionKey partitionKey)
2315
{
@@ -35,7 +27,9 @@ public override string ParsePartitionKey(string id, out PartitionKey partitionKe
3527
}
3628

3729
if (!int.TryParse(parts[1], out int year))
30+
{
3831
throw new ArgumentException("Invalid ID Part");
32+
}
3933

4034
partitionKey = new PartitionKey(year);
4135
return id;

tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
77
using CommunityToolkit.Datasync.Server.CosmosDb.Test.Options;
88
using CommunityToolkit.Datasync.TestCommon;
9-
using CommunityToolkit.Datasync.TestCommon.Databases;
109
using FluentAssertions;
1110
using Microsoft.Azure.Cosmos;
1211
using Microsoft.Azure.Cosmos.Linq;
@@ -57,7 +56,9 @@ protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
5756
}
5857

5958
if (!int.TryParse(parts[1], out int year))
59+
{
6060
throw new ArgumentException("Invalid ID Part");
61+
}
6162

6263
return await this._container.ReadItemAsync<CosmosDbMovie>(id, new PartitionKey(year));
6364
}
@@ -122,7 +123,7 @@ public async Task InitializeAsync()
122123
CompositeIndexes =
123124
{
124125
new Collection<CompositePath>()
125-
{
126+
{
126127
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
127128
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
128129
},
@@ -134,7 +135,7 @@ public async Task InitializeAsync()
134135
}
135136
}
136137
});
137-
138+
138139
foreach (CosmosDbMovie movie in TestData.Movies.OfType<CosmosDbMovie>())
139140
{
140141
movie.Id = $"{Guid.NewGuid()}:{movie.Year}";
@@ -148,7 +149,6 @@ public async Task InitializeAsync()
148149
this._client,
149150
new PackedKeyOptions("Movies", "Movies")
150151
);
151-
152152
}
153153
}
154154

@@ -180,6 +180,7 @@ public async Task ReadAsync_Throws_OnMalformedId(string id)
180180

181181
(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
182182
}
183+
183184
[SkippableTheory]
184185
[InlineData("BadId")]
185186
[InlineData("12345-12345")]
@@ -193,5 +194,4 @@ public async Task DeleteAsync_Throws_OnMalformedIds(string id)
193194
(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
194195
(await GetEntityCountAsync()).Should().Be(TestData.Movies.Count<CosmosDbMovie>());
195196
}
196-
197197
}

0 commit comments

Comments
 (0)