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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@
<PackageVersion Include="TestContainers.MsSql" Version="4.4.0" />
<PackageVersion Include="TestContainers.MySql" Version="4.4.0" />
<PackageVersion Include="TestContainers.PostgreSql" Version="4.4.0" />
<PackageVersion Include="xRetry" Version="1.9.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="Ulid" Version="1.3.4" />

<!-- Do not change XUnit.Combinatorial to v2 (xUnit v2 compatibility) -->
<PackageVersion Include="XUnit.Combinatorial" Version="1.6.24" />
<PackageVersion Include="XUnit.SkippableFact" Version="1.5.23" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
using CommunityToolkit.Datasync.Client.Query.OData;
using CommunityToolkit.Datasync.Client.Serialization;
using CommunityToolkit.Datasync.Client.Threading;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CommunityToolkit.Datasync.Client.Offline.Operations;

Expand Down Expand Up @@ -42,6 +45,7 @@ internal class PullOperationManager(OfflineDbContext context, IEnumerable<Type>
/// <param name="pullOptions">The pull options to use.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>The results of the pull operation.</returns>
[SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Readability")]
public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, PullOptions pullOptions, CancellationToken cancellationToken = default)
{
ArgumentValidationException.ThrowIfNotValid(pullOptions, nameof(pullOptions));
Expand All @@ -67,7 +71,25 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
}
else if (originalEntity is not null && !metadata.Deleted)
{
context.Entry(originalEntity).CurrentValues.SetValues(item);
// Gather properties marked with [JsonIgnore]
HashSet<string> ignoredProps = pullResponse.EntityType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.IsDefined(typeof(JsonIgnoreAttribute), inherit: true))
.Select(p => p.Name)
.ToHashSet();

EntityEntry originalEntry = context.Entry(originalEntity);
EntityEntry newEntry = context.Entry(item);

// Only copy properties that are not marked with [JsonIgnore]
foreach (IProperty property in originalEntry.Metadata.GetProperties())
{
if (!ignoredProps.Contains(property.Name))
{
originalEntry.Property(property.Name).CurrentValue = newEntry.Property(property.Name).CurrentValue;
}
}

result.IncrementReplacements();
}

Expand Down Expand Up @@ -161,12 +183,11 @@ internal async Task<Page<object>> GetPageAsync(HttpClient client, Uri requestUri
object? result = await JsonSerializer.DeserializeAsync(response.ContentStream, pageType, context.JsonSerializerOptions, cancellationToken).ConfigureAwait(false)
?? throw new DatasyncPullException("JSON result is null") { ServiceResponse = response };

Page<object> page = new Page<object>()
return new Page<object>()
{
Items = (IEnumerable<object>)itemsPropInfo.GetValue(result)!,
NextLink = (string?)nextLinkPropInfo.GetValue(result)
};
return page;
}
catch (JsonException ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
using CommunityToolkit.Datasync.Client.Threading;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CommunityToolkit.Datasync.Client.Offline.OperationsQueue;

Expand Down Expand Up @@ -379,6 +381,7 @@ internal async Task<PushResult> PushAsync(IEnumerable<Type> entityTypes, PushOpt
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
[SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Readability")]
internal void ReplaceDatabaseValue(object? oldValue, object? newValue)
{
if (oldValue is null || newValue is null)
Expand All @@ -388,8 +391,21 @@ internal void ReplaceDatabaseValue(object? oldValue, object? newValue)

lock (this.pushlock)
{
EntityEntry tracker = this._context.Entry(oldValue);
tracker.CurrentValues.SetValues(newValue);
EntityEntry oldEntry = this._context.Entry(oldValue);
EntityEntry newEntry = this._context.Entry(newValue);

HashSet<string> ignoredProps = oldValue.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.IsDefined(typeof(JsonIgnoreAttribute), inherit: true))
.Select(p => p.Name)
.ToHashSet();

foreach (IProperty property in oldEntry.Metadata.GetProperties())
{
if (!ignoredProps.Contains(property.Name))
{
oldEntry.Property(property.Name).CurrentValue = newEntry.Property(property.Name).CurrentValue;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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.Server;
using CommunityToolkit.Datasync.TestCommon.Models;
using System.Text.Json.Serialization;

namespace CommunityToolkit.Datasync.Client.Test.Helpers;

/// <summary>
/// This is a copy of the ClientMovie class, but with additional properties
/// that are not synchronized to the server.
/// </summary>
[ExcludeFromCodeCoverage]
public class ClientMovieWithLocalData : ClientTableData, IMovie, IEquatable<IMovie>
{
public ClientMovieWithLocalData() { }

public ClientMovieWithLocalData(object source)
{
if (source is ITableData metadata)
{
Id = metadata.Id;
Deleted = metadata.Deleted;
UpdatedAt = metadata.UpdatedAt;
Version = Convert.ToBase64String(metadata.Version);
}

if (source is IMovie movie)
{
BestPictureWinner = movie.BestPictureWinner;
Duration = movie.Duration;
Rating = movie.Rating;
ReleaseDate = movie.ReleaseDate;
Title = movie.Title;
Year = movie.Year;
}

if (source is ClientMovieWithLocalData localData)
{
UserRating = localData.UserRating;
}
}

/// <summary>
/// A client-only value This value is not synchronized to the server.
/// </summary>
[JsonIgnore]
public int UserRating { get; set; } = 0;

/// <summary>
/// True if the movie won the oscar for Best Picture
/// </summary>
public bool BestPictureWinner { get; set; }

/// <summary>
/// The running time of the movie
/// </summary>
public int Duration { get; set; }

/// <summary>
/// The MPAA rating for the movie, if available.
/// </summary>
public MovieRating Rating { get; set; } = MovieRating.Unrated;

/// <summary>
/// The release date of the movie.
/// </summary>
public DateOnly ReleaseDate { get; set; }

/// <summary>
/// The title of the movie.
/// </summary>
public string Title { get; set; } = string.Empty;

/// <summary>
/// The year that the movie was released.
/// </summary>
public int Year { get; set; }

/// <summary>
/// Determines if this movie has the same content as another movie.
/// </summary>
/// <param name="other">The other movie</param>
/// <returns>true if the content is the same</returns>
public bool Equals(IMovie other)
=> other != null
&& other.BestPictureWinner == BestPictureWinner
&& other.Duration == Duration
&& other.Rating == Rating
&& other.ReleaseDate == ReleaseDate
&& other.Title == Title
&& other.Year == Year;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class IntegrationDbContext(DbContextOptions<IntegrationDbContext> options

public DbSet<ByteVersionMovie> ByteMovies => Set<ByteVersionMovie>();

public DbSet<ClientMovieWithLocalData> MoviesWithLocalData => Set<ClientMovieWithLocalData>();

public ServiceApplicationFactory Factory { get; set; }

public SqliteConnection Connection { get; set; }
Expand All @@ -36,6 +38,12 @@ protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder o
cfg.ClientName = "movies";
cfg.Endpoint = new Uri($"/{Factory.MovieEndpoint}", UriKind.Relative);
});

optionsBuilder.Entity<ClientMovieWithLocalData>(cfg =>
{
cfg.ClientName = "movies";
cfg.Endpoint = new Uri($"/{Factory.MovieEndpoint}", UriKind.Relative);
});
}

protected override void Dispose(bool disposing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,46 @@ await this.context.PullAsync(builder =>
.And.HaveEquivalentMetadataTo(serviceMovie);
}
}

[Fact]
public async Task PullAsync_WithLocalData_Works()
{
const string testId = "id-010";

await this.context.MoviesWithLocalData.PullAsync();

ClientMovieWithLocalData t1 = await this.context.MoviesWithLocalData.FindAsync([testId]);

// Update the local data part and push it back to the server.
t1.UserRating = 5;
this.context.Update(t1);
await this.context.SaveChangesAsync();
await this.context.MoviesWithLocalData.PushAsync();

// Reload the local data from the server and check that the local data is still there
await this.context.Entry(t1).ReloadAsync();
t1.UserRating.Should().Be(5);

// Pull again and check that the local data is still there.
await this.context.MoviesWithLocalData.PullAsync();
ClientMovieWithLocalData t2 = await this.context.MoviesWithLocalData.FindAsync([testId]);
t2.UserRating.Should().Be(5);

// Do another change (this time, server side) and push again
t2.Title = "New Title";
this.context.Update(t2);
await this.context.SaveChangesAsync();
await this.context.MoviesWithLocalData.PushAsync();

// Reload the local data from the server and check that the local data is still there
await this.context.Entry(t2).ReloadAsync();
t2.UserRating.Should().Be(5);
t2.Title.Should().Be("New Title");

// Pull again and check that the local data is still there.
await this.context.MoviesWithLocalData.PullAsync();
ClientMovieWithLocalData t3 = await this.context.MoviesWithLocalData.FindAsync([testId]);
t3.UserRating.Should().Be(5);
t3.Title.Should().Be("New Title");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Ulid" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CommunityToolkit.Datasync.Server.EntityFrameworkCore\CommunityToolkit.Datasync.Server.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\CommunityToolkit.Datasync.TestCommon\CommunityToolkit.Datasync.TestCommon.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -19,5 +12,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Ulid" />
<PackageReference Include="xRetry" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\CommunityToolkit.Datasync.Server.EntityFrameworkCore\CommunityToolkit.Datasync.Server.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\CommunityToolkit.Datasync.TestCommon\CommunityToolkit.Datasync.TestCommon.csproj" />
</ItemGroup>

</Project>