Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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(t1).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>