Skip to content

Commit b726c2a

Browse files
author
Adrian Hall
committed
(#298) Updated the conflict resolver to avoid the extra problematic round trip when server wins is chosen.
1 parent 06c6afa commit b726c2a

File tree

6 files changed

+239
-55
lines changed

6 files changed

+239
-55
lines changed

src/CommunityToolkit.Datasync.Client/Offline/ConflictResolvers.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace CommunityToolkit.Datasync.Client.Offline;
1212
public abstract class AbstractConflictResolver<TEntity> : IConflictResolver<TEntity>
1313
{
1414
/// <inheritdoc />
15-
public abstract Task<TEntity?> ResolveConflictAsync(TEntity? clientObject, TEntity? serverObject, CancellationToken cancellationToken = default);
15+
public abstract Task<ConflictResolution> ResolveConflictAsync(TEntity? clientObject, TEntity? serverObject, CancellationToken cancellationToken = default);
1616

1717
/// <summary>
1818
/// The object version of the resolver calls the typed version.
@@ -21,7 +21,7 @@ public abstract class AbstractConflictResolver<TEntity> : IConflictResolver<TEnt
2121
/// <param name="serverObject"></param>
2222
/// <param name="cancellationToken"></param>
2323
/// <returns></returns>
24-
public virtual async Task<object?> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
24+
public virtual async Task<ConflictResolution> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
2525
=> await ResolveConflictAsync((TEntity?)clientObject, (TEntity?)serverObject, cancellationToken);
2626
}
2727

@@ -31,8 +31,12 @@ public abstract class AbstractConflictResolver<TEntity> : IConflictResolver<TEnt
3131
public class ClientWinsConflictResolver : IConflictResolver
3232
{
3333
/// <inheritdoc />
34-
public Task<object?> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
35-
=> Task.FromResult(clientObject);
34+
public Task<ConflictResolution> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
35+
=> Task.FromResult(new ConflictResolution
36+
{
37+
Result = ConflictResolutionResult.Client,
38+
Entity = clientObject
39+
});
3640
}
3741

3842
/// <summary>
@@ -41,7 +45,11 @@ public class ClientWinsConflictResolver : IConflictResolver
4145
public class ServerWinsConflictResolver : IConflictResolver
4246
{
4347
/// <inheritdoc />
44-
public Task<object?> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
45-
=> Task.FromResult(serverObject);
48+
public Task<ConflictResolution> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default)
49+
=> Task.FromResult(new ConflictResolution
50+
{
51+
Result = ConflictResolutionResult.Server,
52+
Entity = serverObject
53+
});
4654
}
4755

src/CommunityToolkit.Datasync.Client/Offline/IConflictResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public interface IConflictResolver
1818
/// <param name="serverObject">The server object.</param>
1919
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
2020
/// <returns>The conflict resolution.</returns>
21-
Task<object?> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default);
21+
Task<ConflictResolution> ResolveConflictAsync(object? clientObject, object? serverObject, CancellationToken cancellationToken = default);
2222
}
2323

2424
/// <summary>
@@ -36,5 +36,5 @@ public interface IConflictResolver<TEntity> : IConflictResolver
3636
/// <param name="serverObject">The server object.</param>
3737
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
3838
/// <returns>The conflict resolution.</returns>
39-
Task<TEntity?> ResolveConflictAsync(TEntity? clientObject, TEntity? serverObject, CancellationToken cancellationToken = default);
39+
Task<ConflictResolution> ResolveConflictAsync(TEntity? clientObject, TEntity? serverObject, CancellationToken cancellationToken = default);
4040
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
namespace CommunityToolkit.Datasync.Client.Offline;
6+
7+
/// <summary>
8+
/// The possible results of a conflict resolution.
9+
/// </summary>
10+
public enum ConflictResolutionResult
11+
{
12+
/// <summary>
13+
/// The default resolution, which is to do nothing and re-queue the operation.
14+
/// </summary>
15+
Default,
16+
17+
/// <summary>
18+
/// The provided client object should be used. This results in a new "force" submission
19+
/// to the server to over-write the server entity.
20+
/// </summary>
21+
Client,
22+
23+
/// <summary>
24+
/// The server object should be used. This results in the client object being updated
25+
/// with whatever the server object was provided.
26+
/// </summary>
27+
Server
28+
}
29+
30+
/// <summary>
31+
/// The model class returned by a conflict resolver to indicate the resolution of the conflict.
32+
/// </summary>
33+
public class ConflictResolution
34+
{
35+
/// <summary>
36+
/// The conflict resolution result.
37+
/// </summary>
38+
public ConflictResolutionResult Result { get; set; } = ConflictResolutionResult.Default;
39+
40+
/// <summary>
41+
/// The entity, if required.
42+
/// </summary>
43+
public object? Entity { get; set; }
44+
}

src/CommunityToolkit.Datasync.Client/Offline/Operations/PullOperationManager.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,19 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
5959
{
6060
_ = context.Add(item);
6161
result.IncrementAdditions();
62-
}
62+
}
6363
else if (originalEntity is not null && metadata.Deleted)
6464
{
6565
_ = context.Remove(originalEntity);
6666
result.IncrementDeletions();
67-
}
67+
}
6868
else if (originalEntity is not null && !metadata.Deleted)
6969
{
7070
context.Entry(originalEntity).CurrentValues.SetValues(item);
7171
result.IncrementReplacements();
7272
}
7373

74-
if (metadata.UpdatedAt.HasValue && metadata.UpdatedAt.Value > lastSynchronization)
74+
if (metadata.UpdatedAt > lastSynchronization)
7575
{
7676
lastSynchronization = metadata.UpdatedAt.Value;
7777
bool isAdded = await DeltaTokenStore.SetDeltaTokenAsync(pullResponse.QueryId, metadata.UpdatedAt.Value, cancellationToken).ConfigureAwait(false);

src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/OperationsQueueManager.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using CommunityToolkit.Datasync.Client.Threading;
99
using Microsoft.EntityFrameworkCore;
1010
using Microsoft.EntityFrameworkCore.ChangeTracking;
11+
using System.Net;
1112
using System.Reflection;
1213
using System.Text.Json;
1314

@@ -77,7 +78,7 @@ internal List<EntityEntry> GetChangedEntitiesInScope()
7778
/// </summary>
7879
/// <remarks>
7980
/// An entity is "synchronization ready" if:
80-
///
81+
///
8182
/// * It is a property on this context
8283
/// * The property is public and a <see cref="DbSet{TEntity}"/>.
8384
/// * The property does not have a <see cref="DoNotSynchronizeAttribute"/> specified.
@@ -215,7 +216,7 @@ internal IEnumerable<Type> GetSynchronizableEntityTypes(IEnumerable<Type> allowe
215216
/// </summary>
216217
/// <remarks>
217218
/// An entity is "synchronization ready" if:
218-
///
219+
///
219220
/// * It is a property on this context
220221
/// * The property is public and a <see cref="DbSet{TEntity}"/>.
221222
/// * The property does not have a <see cref="DoNotSynchronizeAttribute"/> specified.
@@ -299,30 +300,40 @@ internal async Task<PushResult> PushAsync(IEnumerable<Type> entityTypes, PushOpt
299300
ExecutableOperation op = await ExecutableOperation.CreateAsync(operation, cancellationToken).ConfigureAwait(false);
300301
ServiceResponse response = await op.ExecuteAsync(options, cancellationToken).ConfigureAwait(false);
301302

303+
bool isSuccessful = response.IsSuccessful;
302304
if (response.IsConflictStatusCode && options.ConflictResolver is not null)
303305
{
304306
object? serverEntity = JsonSerializer.Deserialize(response.ContentStream, entityType, DatasyncSerializer.JsonSerializerOptions);
305307
object? clientEntity = JsonSerializer.Deserialize(operation.Item, entityType, DatasyncSerializer.JsonSerializerOptions);
306-
object? resolvedEntity = await options.ConflictResolver.ResolveConflictAsync(clientEntity, serverEntity, cancellationToken).ConfigureAwait(false);
307-
if (resolvedEntity is not null)
308+
ConflictResolution resolution = await options.ConflictResolver.ResolveConflictAsync(clientEntity, serverEntity, cancellationToken).ConfigureAwait(false);
309+
310+
if (resolution.Result is ConflictResolutionResult.Client)
311+
{
312+
operation.Item = JsonSerializer.Serialize(resolution.Entity, entityType, DatasyncSerializer.JsonSerializerOptions);
313+
operation.State = OperationState.Pending;
314+
operation.LastAttempt = DateTimeOffset.UtcNow;
315+
operation.HttpStatusCode = response.StatusCode;
316+
operation.EntityVersion = string.Empty; // Force the push
317+
operation.Version++;
318+
_ = this._context.Update(operation);
319+
ExecutableOperation resolvedOp = await ExecutableOperation.CreateAsync(operation, cancellationToken).ConfigureAwait(false);
320+
response = await resolvedOp.ExecuteAsync(options, cancellationToken).ConfigureAwait(false);
321+
isSuccessful = response.IsSuccessful;
322+
}
323+
else if (resolution.Result is ConflictResolutionResult.Server)
308324
{
309325
lock (this.pushlock)
310326
{
311-
operation.Item = JsonSerializer.Serialize(resolvedEntity, entityType, DatasyncSerializer.JsonSerializerOptions);
312-
operation.State = OperationState.Pending;
327+
operation.State = OperationState.Completed; // Make it successful
313328
operation.LastAttempt = DateTimeOffset.UtcNow;
314-
operation.HttpStatusCode = response.StatusCode;
315-
operation.EntityVersion = string.Empty; // Force the push
316-
operation.Version++;
329+
operation.HttpStatusCode = 200;
330+
isSuccessful = true;
317331
_ = this._context.Update(operation);
318332
}
319-
320-
ExecutableOperation resolveOperation = await ExecutableOperation.CreateAsync(operation, cancellationToken).ConfigureAwait(false);
321-
response = await resolveOperation.ExecuteAsync(options, cancellationToken).ConfigureAwait(false);
322333
}
323334
}
324335

325-
if (!response.IsSuccessful)
336+
if (!isSuccessful)
326337
{
327338
lock (this.pushlock)
328339
{
@@ -338,6 +349,7 @@ internal async Task<PushResult> PushAsync(IEnumerable<Type> entityTypes, PushOpt
338349
// If the operation is a success, then the content may need to be updated.
339350
if (operation.Kind != OperationKind.Delete)
340351
{
352+
_ = response.ContentStream.Seek(0L, SeekOrigin.Begin); // Reset the memory stream to the beginning.
341353
object? newValue = JsonSerializer.Deserialize(response.ContentStream, entityType, DatasyncSerializer.JsonSerializerOptions);
342354
object? oldValue = await this._context.FindAsync(entityType, [operation.ItemId], cancellationToken).ConfigureAwait(false);
343355
ReplaceDatabaseValue(oldValue, newValue);

0 commit comments

Comments
 (0)