Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
[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<DynamicProxiesTestContext> dbContextOptions = new DbContextOptionsBuilder<DynamicProxiesTestContext>()
.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<DynamicProxiesEntity1> DynamicProxiesEntities1 { get; set; }

public virtual DbSet<DynamicProxiesEntity2> 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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +41,7 @@ protected virtual void Dispose(bool disposing)
}

override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString);

protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
{
try
Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection<CompositePath>()
{
{
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
Expand All @@ -121,7 +121,7 @@ public async Task InitializeAsync()
}
}
});

foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType<CosmosDbMovie>())
{
movie.UpdatedAt = DateTimeOffset.UtcNow;
Expand All @@ -134,7 +134,6 @@ public async Task InitializeAsync()
this._client,
new CosmosSingleTableOptions<CosmosDbMovie>("Movies", "Movies")
);

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>();
CCosmosTableData entity_b = b.ToTableEntity<CCosmosTableData>();

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
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CosmosDbMovie>
public class PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true)
: CosmosSingleTableOptions<CosmosDbMovie>(databaseId, containerId, shouldUpdateTimestamp)
{
public PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
{
}

public override Func<CosmosDbMovie, string> IdGenerator => (entity) => $"{Guid.NewGuid()}:{entity.Year}";
public override string GetPartitionKey(CosmosDbMovie entity, out PartitionKey partitionKey)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,7 +56,9 @@ protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
}

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

return await this._container.ReadItemAsync<CosmosDbMovie>(id, new PartitionKey(year));
}
Expand Down Expand Up @@ -122,7 +123,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection<CompositePath>()
{
{
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
Expand All @@ -134,7 +135,7 @@ public async Task InitializeAsync()
}
}
});

foreach (CosmosDbMovie movie in TestData.Movies.OfType<CosmosDbMovie>())
{
movie.Id = $"{Guid.NewGuid()}:{movie.Year}";
Expand All @@ -148,7 +149,6 @@ public async Task InitializeAsync()
this._client,
new PackedKeyOptions("Movies", "Movies")
);

}
}

Expand Down Expand Up @@ -180,6 +180,7 @@ public async Task ReadAsync_Throws_OnMalformedId(string id)

(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
}

[SkippableTheory]
[InlineData("BadId")]
[InlineData("12345-12345")]
Expand All @@ -193,5 +194,4 @@ public async Task DeleteAsync_Throws_OnMalformedIds(string id)
(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
(await GetEntityCountAsync()).Should().Be(TestData.Movies.Count<CosmosDbMovie>());
}

}