Skip to content

Commit e232d8c

Browse files
committed
(#384) Added SynchronizationEventType PullStarted and PullEnded.
Added Exception and ServiceResponse to SynchronizationEventArgs. Fixed bug in SynchronizationProgress_Event_Works test. Initialization of eventFired was wrong. SynchronizationProgress_Event_Works added tests for start and end.
1 parent d4e996d commit e232d8c

File tree

3 files changed

+124
-56
lines changed

3 files changed

+124
-56
lines changed

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

Lines changed: 83 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Reflection;
1515
using System.Text.Json;
1616
using System.Text.Json.Serialization;
17+
using static CommunityToolkit.Datasync.Client.Offline.Operations.PullOperationManager;
1718

1819
namespace CommunityToolkit.Datasync.Client.Offline.Operations;
1920

@@ -53,70 +54,87 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
5354

5455
QueueHandler<PullResponse> databaseUpdateQueue = new(1, async pullResponse =>
5556
{
56-
DateTimeOffset lastSynchronization = await DeltaTokenStore.GetDeltaTokenAsync(pullResponse.QueryId, cancellationToken).ConfigureAwait(false);
57-
foreach (object item in pullResponse.Items)
57+
if (pullResponse.Items.Any())
5858
{
59-
EntityMetadata metadata = EntityResolver.GetEntityMetadata(item, pullResponse.EntityType);
60-
object? originalEntity = await context.FindAsync(pullResponse.EntityType, [metadata.Id], cancellationToken).ConfigureAwait(false);
61-
62-
if (originalEntity is null && !metadata.Deleted)
63-
{
64-
_ = context.Add(item);
65-
result.IncrementAdditions();
66-
}
67-
else if (originalEntity is not null && metadata.Deleted)
59+
DateTimeOffset lastSynchronization = await DeltaTokenStore.GetDeltaTokenAsync(pullResponse.QueryId, cancellationToken).ConfigureAwait(false);
60+
foreach (object item in pullResponse.Items)
6861
{
69-
_ = context.Remove(originalEntity);
70-
result.IncrementDeletions();
71-
}
72-
else if (originalEntity is not null && !metadata.Deleted)
73-
{
74-
// Gather properties marked with [JsonIgnore]
75-
HashSet<string> ignoredProps = pullResponse.EntityType
76-
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
77-
.Where(p => p.IsDefined(typeof(JsonIgnoreAttribute), inherit: true))
78-
.Select(p => p.Name)
79-
.ToHashSet();
80-
81-
EntityEntry originalEntry = context.Entry(originalEntity);
82-
EntityEntry newEntry = context.Entry(item);
62+
EntityMetadata metadata = EntityResolver.GetEntityMetadata(item, pullResponse.EntityType);
63+
object? originalEntity = await context.FindAsync(pullResponse.EntityType, [metadata.Id], cancellationToken).ConfigureAwait(false);
8364

84-
// Only copy properties that are not marked with [JsonIgnore]
85-
foreach (IProperty property in originalEntry.Metadata.GetProperties())
65+
if (originalEntity is null && !metadata.Deleted)
8666
{
87-
if (!ignoredProps.Contains(property.Name))
67+
_ = context.Add(item);
68+
result.IncrementAdditions();
69+
}
70+
else if (originalEntity is not null && metadata.Deleted)
71+
{
72+
_ = context.Remove(originalEntity);
73+
result.IncrementDeletions();
74+
}
75+
else if (originalEntity is not null && !metadata.Deleted)
76+
{
77+
// Gather properties marked with [JsonIgnore]
78+
HashSet<string> ignoredProps = pullResponse.EntityType
79+
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
80+
.Where(p => p.IsDefined(typeof(JsonIgnoreAttribute), inherit: true))
81+
.Select(p => p.Name)
82+
.ToHashSet();
83+
84+
EntityEntry originalEntry = context.Entry(originalEntity);
85+
EntityEntry newEntry = context.Entry(item);
86+
87+
// Only copy properties that are not marked with [JsonIgnore]
88+
foreach (IProperty property in originalEntry.Metadata.GetProperties())
8889
{
89-
originalEntry.Property(property.Name).CurrentValue = newEntry.Property(property.Name).CurrentValue;
90+
if (!ignoredProps.Contains(property.Name))
91+
{
92+
originalEntry.Property(property.Name).CurrentValue = newEntry.Property(property.Name).CurrentValue;
93+
}
9094
}
95+
96+
result.IncrementReplacements();
9197
}
9298

93-
result.IncrementReplacements();
99+
if (metadata.UpdatedAt > lastSynchronization)
100+
{
101+
lastSynchronization = metadata.UpdatedAt.Value;
102+
bool isAdded = await DeltaTokenStore.SetDeltaTokenAsync(pullResponse.QueryId, metadata.UpdatedAt.Value, cancellationToken).ConfigureAwait(false);
103+
if (isAdded)
104+
{
105+
// Sqlite oddity - you can't add then update; it changes the change type to UPDATE, which then fails.
106+
_ = await context.SaveChangesAsync(true, false, cancellationToken).ConfigureAwait(false);
107+
}
108+
}
94109
}
95110

96-
if (metadata.UpdatedAt > lastSynchronization)
111+
if (pullOptions.SaveAfterEveryServiceRequest)
97112
{
98-
lastSynchronization = metadata.UpdatedAt.Value;
99-
bool isAdded = await DeltaTokenStore.SetDeltaTokenAsync(pullResponse.QueryId, metadata.UpdatedAt.Value, cancellationToken).ConfigureAwait(false);
100-
if (isAdded)
101-
{
102-
// Sqlite oddity - you can't add then update; it changes the change type to UPDATE, which then fails.
103-
_ = await context.SaveChangesAsync(true, false, cancellationToken).ConfigureAwait(false);
104-
}
113+
_ = await context.SaveChangesAsync(true, false, cancellationToken).ConfigureAwait(false);
105114
}
106-
}
107115

108-
context.SendSynchronizationEvent(new SynchronizationEventArgs()
109-
{
110-
EventType = SynchronizationEventType.ItemsCommitted,
111-
EntityType = pullResponse.EntityType,
112-
ItemsProcessed = pullResponse.TotalItemsProcessed,
113-
TotalNrItems = pullResponse.TotalRequestItems,
114-
QueryId = pullResponse.QueryId
115-
});
116+
context.SendSynchronizationEvent(new SynchronizationEventArgs()
117+
{
118+
EventType = SynchronizationEventType.ItemsCommitted,
119+
EntityType = pullResponse.EntityType,
120+
ItemsProcessed = pullResponse.TotalItemsProcessed,
121+
TotalNrItems = pullResponse.TotalRequestItems,
122+
QueryId = pullResponse.QueryId
123+
});
124+
}
116125

117-
if (pullOptions.SaveAfterEveryServiceRequest)
126+
if (pullResponse.Completed)
118127
{
119-
_ = await context.SaveChangesAsync(true, false, cancellationToken).ConfigureAwait(false);
128+
context.SendSynchronizationEvent(new SynchronizationEventArgs()
129+
{
130+
EventType = SynchronizationEventType.PullEnded,
131+
EntityType = pullResponse.EntityType,
132+
ItemsProcessed = pullResponse.TotalItemsProcessed,
133+
TotalNrItems = pullResponse.TotalRequestItems,
134+
QueryId = pullResponse.QueryId,
135+
Exception = pullResponse.Exception,
136+
ServiceResponse = pullResponse.Exception is DatasyncPullException ex ? ex.ServiceResponse : null
137+
});
120138
}
121139
});
122140

@@ -125,15 +143,24 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
125143
Uri endpoint = ExecutableOperation.MakeAbsoluteUri(pullRequest.HttpClient.BaseAddress, pullRequest.Endpoint);
126144
Uri requestUri = new UriBuilder(endpoint) { Query = pullRequest.QueryDescription.ToODataQueryString() }.Uri;
127145
Type pageType = typeof(Page<>).MakeGenericType(pullRequest.EntityType);
146+
long itemsProcessed = 0;
147+
long totalCount = 0;
128148

129149
try
130150
{
131151
bool completed = false;
132-
long itemsProcessed = 0;
152+
// Signal we started the pull operation.
153+
context.SendSynchronizationEvent(new SynchronizationEventArgs()
154+
{
155+
EventType = SynchronizationEventType.PullStarted,
156+
EntityType = pullRequest.EntityType,
157+
QueryId = pullRequest.QueryId
158+
});
133159
do
134160
{
135161
Page<object> page = await GetPageAsync(pullRequest.HttpClient, requestUri, pageType, cancellationToken).ConfigureAwait(false);
136162
itemsProcessed += page.Items.Count();
163+
totalCount = page.Count ?? totalCount;
137164

138165
context.SendSynchronizationEvent(new SynchronizationEventArgs()
139166
{
@@ -144,7 +171,6 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
144171
QueryId = pullRequest.QueryId
145172
});
146173

147-
databaseUpdateQueue.Enqueue(new PullResponse(pullRequest.EntityType, pullRequest.QueryId, page.Items, page.Count ?? 0, itemsProcessed));
148174
if (!string.IsNullOrEmpty(page.NextLink))
149175
{
150176
requestUri = new UriBuilder(endpoint) { Query = page.NextLink }.Uri;
@@ -153,12 +179,15 @@ public async Task<PullResult> ExecuteAsync(IEnumerable<PullRequest> requests, Pu
153179
{
154180
completed = true;
155181
}
182+
183+
databaseUpdateQueue.Enqueue(new PullResponse(pullRequest.EntityType, pullRequest.QueryId, page.Items, totalCount, itemsProcessed, completed));
156184
}
157185
while (!completed);
158186
}
159187
catch (DatasyncPullException ex)
160188
{
161189
result.AddFailedRequest(requestUri, ex.ServiceResponse);
190+
databaseUpdateQueue.Enqueue(new PullResponse(pullRequest.EntityType, pullRequest.QueryId, Enumerable.Empty<object>(), totalCount, itemsProcessed, true, ex));
162191
}
163192
});
164193

@@ -263,6 +292,8 @@ internal static QueryDescription PrepareQueryDescription(QueryDescription source
263292
/// <param name="Items">The list of items to process.</param>
264293
/// <param name="TotalRequestItems">The total number of items in the current pull request.</param>
265294
/// <param name="TotalItemsProcessed">The total number of items processed, <paramref name="Items"/> included.</param>
295+
/// <param name="Completed">If <c>true</c>, indicates that the pull request is completed.</param>
296+
/// <param name="Exception">Indicates an exception occured during fetching of data</param>
266297
[ExcludeFromCodeCoverage]
267-
internal record PullResponse(Type EntityType, string QueryId, IEnumerable<object> Items, long TotalRequestItems, long TotalItemsProcessed);
298+
internal record PullResponse(Type EntityType, string QueryId, IEnumerable<object> Items, long TotalRequestItems, long TotalItemsProcessed, bool Completed, Exception? Exception = null);
268299
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ namespace CommunityToolkit.Datasync.Client.Offline;
99
/// </summary>
1010
public enum SynchronizationEventType
1111
{
12+
/// <summary>
13+
/// Pull for the given entity starts.
14+
/// </summary>
15+
/// <remarks><see cref="SynchronizationEventArgs.ItemsProcessed"/> is not yet known here</remarks>
16+
PullStarted,
1217
/// <summary>
1318
/// Occurs when items have been successfully fetches from the server.
1419
/// </summary>
@@ -20,6 +25,11 @@ public enum SynchronizationEventType
2025
/// </summary>
2126
/// <remarks>This event is raised after a page of entities was succesfully commited to the database</remarks>
2227
ItemsCommitted,
28+
29+
/// <summary>
30+
/// Pull for the given entiry ended.
31+
/// </summary>
32+
PullEnded,
2333
}
2434

2535
/// <summary>
@@ -51,4 +61,14 @@ public class SynchronizationEventArgs
5161
/// The query ID that is being processed
5262
/// </summary>
5363
public required string QueryId { get; init; }
64+
65+
/// <summary>
66+
/// If not <c>null</c> on event type <see cref="SynchronizationEventType.PullEnded"/>, indicates pull failed with this exception.
67+
/// </summary>
68+
public Exception? Exception { get; init; }
69+
70+
/// <summary>
71+
/// If a <see cref="DatasyncException"/> occured in <see cref="Exception"/> during server call processing, this property has more detail on the server response.
72+
/// </summary>
73+
public ServiceResponse? ServiceResponse { get; init; }
5474
}

tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,8 +1447,10 @@ public async Task SynchronizationProgress_Event_Works()
14471447
this.context.Handler.AddResponse(HttpStatusCode.OK, page3);
14481448
this.context.Handler.AddResponse(HttpStatusCode.OK, page4);
14491449

1450-
bool eventFiredForFetch = true;
1451-
bool eventFiredForCommit = true;
1450+
bool eventFiredForFetch = false;
1451+
bool eventFiredForCommit = false;
1452+
bool eventFiredForStart = false;
1453+
bool eventFiredForEnd = false;
14521454
long currentItemsFetched = 0;
14531455
long currentItemsCommited = 0;
14541456

@@ -1457,19 +1459,32 @@ public async Task SynchronizationProgress_Event_Works()
14571459
sender.Should().Be(this.context);
14581460
args.EntityType.Should().Be<ClientMovie>();
14591461
args.QueryId.Should().Be("CommunityToolkit.Datasync.TestCommon.Databases.ClientMovie");
1460-
args.TotalNrItems.Should().Be(20);
1461-
switch(args.EventType)
1462+
args.Exception.Should().BeNull(); // We don't test exceptions here, so should always be null.
1463+
args.ServiceResponse.Should().BeNull();
1464+
switch (args.EventType)
14621465
{
14631466
case SynchronizationEventType.ItemsFetched:
14641467
currentItemsFetched += 5;
14651468
args.ItemsProcessed.Should().Be(currentItemsFetched);
1469+
args.TotalNrItems.Should().Be(20);
14661470
eventFiredForFetch = true;
14671471
break;
14681472
case SynchronizationEventType.ItemsCommitted:
14691473
currentItemsCommited += 5;
14701474
args.ItemsProcessed.Should().Be(currentItemsCommited);
1475+
args.TotalNrItems.Should().Be(20);
14711476
eventFiredForCommit = true;
14721477
break;
1478+
case SynchronizationEventType.PullStarted:
1479+
eventFiredForStart.Should().BeFalse("PullStarted event should only fire once");
1480+
eventFiredForStart = true;
1481+
break;
1482+
case SynchronizationEventType.PullEnded:
1483+
eventFiredForEnd.Should().BeFalse("PullEnded event should only fire once");
1484+
eventFiredForEnd = true;
1485+
args.ItemsProcessed.Should().Be(20);
1486+
args.TotalNrItems.Should().Be(20);
1487+
break;
14731488
default:
14741489
Assert.Fail($"Invalid event type: {args.EventType}");
14751490
break;
@@ -1478,8 +1493,10 @@ public async Task SynchronizationProgress_Event_Works()
14781493

14791494
await this.context.Movies.PullAsync();
14801495

1496+
eventFiredForStart.Should().BeTrue();
14811497
eventFiredForFetch.Should().BeTrue();
14821498
eventFiredForCommit.Should().BeTrue();
1499+
eventFiredForEnd.Should().BeTrue();
14831500
currentItemsFetched.Should().Be(20);
14841501
currentItemsCommited.Should().Be(20);
14851502
}

0 commit comments

Comments
 (0)