Skip to content

Commit 08482b3

Browse files
authored
(#38) Basic pull operation. (#72)
1 parent fe49650 commit 08482b3

File tree

61 files changed

+5099
-2119
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+5099
-2119
lines changed

.editorconfig

+7-1
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,10 @@ dotnet_diagnostic.SA1629.severity = none
386386

387387
dotnet_diagnostic.SA1633.severity = none
388388
dotnet_diagnostic.SA1634.severity = none
389-
dotnet_diagnostic.SA1652.severity = none
389+
dotnet_diagnostic.SA1652.severity = none
390+
391+
# Regular Expression form - We have a lot of regular expressions that are not in the form of a verbatim string.
392+
dotnet_diagnostic.SYSLIB1045.severity = none
393+
394+
# Namespace requirements - it's ok to have a namespace that doesn't match the folder structure.
395+
dotnet_diagnostic.IDE0130.severity = none

src/CommunityToolkit.Datasync.Client/Exceptions/ConflictException.cs

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.Datasync.Client.Service;
6-
75
namespace CommunityToolkit.Datasync.Client;
86

97
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Datasync.Client.Exceptions;
8+
9+
/// <summary>
10+
/// An internal exception generated during the pull operation.
11+
/// </summary>
12+
internal class DatasyncPullException : DatasyncException
13+
{
14+
/// <inheritdoc />
15+
[ExcludeFromCodeCoverage]
16+
public DatasyncPullException()
17+
{
18+
}
19+
20+
/// <inheritdoc />
21+
[ExcludeFromCodeCoverage]
22+
public DatasyncPullException(string? message) : base(message)
23+
{
24+
}
25+
26+
/// <inheritdoc />
27+
[ExcludeFromCodeCoverage]
28+
public DatasyncPullException(string? message, Exception? innerException) : base(message, innerException)
29+
{
30+
}
31+
32+
/// <summary>
33+
/// The service response for the error.
34+
/// </summary>
35+
public required ServiceResponse ServiceResponse { get; init; }
36+
}

src/CommunityToolkit.Datasync.Client/Http/HttpClientFactory.cs

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Net;
6-
75
namespace CommunityToolkit.Datasync.Client.Http;
86

97
/// <summary>

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

+51-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using CommunityToolkit.Datasync.Client.Http;
6-
using CommunityToolkit.Datasync.Client.Offline.Internal;
6+
using CommunityToolkit.Datasync.Client.Offline.Models;
7+
using CommunityToolkit.Datasync.Client.Query.Linq;
78

89
namespace CommunityToolkit.Datasync.Client.Offline;
910

@@ -83,8 +84,21 @@ public DatasyncOfflineOptionsBuilder UseHttpClientOptions(HttpClientOptions clie
8384
/// <typeparam name="TEntity">The type of the entity.</typeparam>
8485
/// <param name="configure">A configuration function for the entity.</param>
8586
/// <returns>The current builder for chaining.</returns>
86-
public DatasyncOfflineOptionsBuilder Entity<TEntity>(Action<EntityOfflineOptions> configure)
87-
=> Entity(typeof(TEntity), configure);
87+
public DatasyncOfflineOptionsBuilder Entity<TEntity>(Action<EntityOfflineOptions<TEntity>> configure) where TEntity : class
88+
{
89+
ArgumentNullException.ThrowIfNull(configure, nameof(configure));
90+
if (!this._entities.TryGetValue(typeof(TEntity).FullName!, out EntityOfflineOptions? options))
91+
{
92+
throw new DatasyncException($"Entity is not synchronizable.");
93+
}
94+
95+
EntityOfflineOptions<TEntity> entity = new();
96+
configure(entity);
97+
options.ClientName = entity.ClientName;
98+
options.Endpoint = entity.Endpoint;
99+
options.QueryDescription = new QueryTranslator<TEntity>(entity.Query).Translate();
100+
return this;
101+
}
88102

89103
/// <summary>
90104
/// Configures the specified entity type for offline operations.
@@ -120,9 +134,10 @@ internal OfflineOptions Build()
120134
{
121135
HttpClientFactory = this._httpClientFactory
122136
};
137+
123138
foreach (EntityOfflineOptions entity in this._entities.Values)
124139
{
125-
result.AddEntity(entity.EntityType, entity.ClientName, entity.Endpoint);
140+
result.AddEntity(entity.EntityType, entity.ClientName, entity.Endpoint, entity.QueryDescription);
126141
}
127142

128143
return result;
@@ -139,14 +154,46 @@ public class EntityOfflineOptions(Type entityType)
139154
/// </summary>
140155
public Type EntityType { get => entityType; }
141156

157+
/// <summary>
158+
/// The name of the client to use when requesting a <see cref="HttpClient"/>.
159+
/// </summary>
160+
public string ClientName { get; set; } = string.Empty;
161+
142162
/// <summary>
143163
/// The endpoint for the entity type.
144164
/// </summary>
145165
public Uri Endpoint { get; set; } = new Uri($"/tables/{entityType.Name.ToLowerInvariant()}", UriKind.Relative);
146166

167+
/// <summary>
168+
/// The query description for the entity type - may be null (to mean "pull everything").
169+
/// </summary>
170+
internal QueryDescription? QueryDescription { get; set; }
171+
}
172+
173+
/// <summary>
174+
/// A typed version of the <see cref="EntityOfflineOptions"/> for setting up queries.
175+
/// </summary>
176+
/// <typeparam name="TEntity">The type of entity being stored.</typeparam>
177+
public class EntityOfflineOptions<TEntity>() where TEntity : class
178+
{
179+
/// <summary>
180+
/// The entity type being configured.
181+
/// </summary>
182+
public Type EntityType { get => typeof(TEntity); }
183+
147184
/// <summary>
148185
/// The name of the client to use when requesting a <see cref="HttpClient"/>.
149186
/// </summary>
150187
public string ClientName { get; set; } = string.Empty;
188+
189+
/// <summary>
190+
/// The endpoint for the entity type.
191+
/// </summary>
192+
public Uri Endpoint { get; set; } = new Uri($"/tables/{typeof(TEntity).Name.ToLowerInvariant()}", UriKind.Relative);
193+
194+
/// <summary>
195+
/// The query used to pull the data from the service.
196+
/// </summary>
197+
public IDatasyncPullQuery<TEntity> Query = new DatasyncPullQuery<TEntity>();
151198
}
152199
}

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

-53
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
using System.ComponentModel.DataAnnotations;
6+
7+
namespace CommunityToolkit.Datasync.Client.Offline;
8+
9+
/// <summary>
10+
/// A mapping for the delta-token store, which records the last sequence number of
11+
/// the entity that was synchronized (normally a time stamp).
12+
/// </summary>
13+
public class DatasyncDeltaToken
14+
{
15+
/// <summary>
16+
/// The ID of the entity type.
17+
/// </summary>
18+
[Key]
19+
public required string Id { get; set; }
20+
21+
/// <summary>
22+
/// The value of the sequence number.
23+
/// </summary>
24+
public required long Value { get; set; }
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
using CommunityToolkit.Datasync.Client.Serialization;
6+
7+
namespace CommunityToolkit.Datasync.Client.Offline.DeltaTokenStore;
8+
9+
/// <summary>
10+
/// A default implementation of the <see cref="DeltaTokenStore"/> that will
11+
/// store the delta tokens for each query in an Entity Framework table.
12+
/// </summary>
13+
internal class DefaultDeltaTokenStore(OfflineDbContext context) : IDeltaTokenStore
14+
{
15+
/// <summary>
16+
/// Obtains the current delta token for a table/queryId from persistent store.
17+
/// </summary>
18+
/// <param name="queryId">The query ID of the table.</param>
19+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
20+
/// <returns>A task that returns the delta token when complete.</returns>
21+
public async Task<DateTimeOffset> GetDeltaTokenAsync(string queryId, CancellationToken cancellationToken = default)
22+
{
23+
ValidateQueryId(queryId);
24+
DatasyncDeltaToken? deltaToken = await context.DatasyncDeltaTokens.FindAsync([queryId], cancellationToken).ConfigureAwait(false);
25+
long unixms = deltaToken?.Value ?? 0L;
26+
return DateTimeOffset.FromUnixTimeMilliseconds(unixms);
27+
}
28+
29+
/// <summary>
30+
/// Resets the delta token for a table/queryId from persistent store.
31+
/// </summary>
32+
/// <param name="queryId">The query ID of the table.</param>
33+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
34+
/// <returns>A task that completes when the delta token has been reset.</returns>
35+
public async Task ResetDeltaTokenAsync(string queryId, CancellationToken cancellationToken = default)
36+
{
37+
ValidateQueryId(queryId);
38+
DatasyncDeltaToken? deltaToken = await context.DatasyncDeltaTokens.FindAsync([queryId], cancellationToken).ConfigureAwait(false);
39+
if (deltaToken is not null)
40+
{
41+
_ = context.DatasyncDeltaTokens.Remove(deltaToken);
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Sets the delta token for a table/queryId from persistent store.
47+
/// </summary>
48+
/// <param name="queryId">The query ID of the table.</param>
49+
/// <param name="value">The value of the delta token.</param>
50+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
51+
/// <returns>A task that completes when the delta token has been set in the persistent store.</returns>
52+
public async Task SetDeltaTokenAsync(string queryId, DateTimeOffset value, CancellationToken cancellationToken = default)
53+
{
54+
ValidateQueryId(queryId);
55+
long unixms = value.ToUnixTimeMilliseconds();
56+
DatasyncDeltaToken? deltaToken = await context.DatasyncDeltaTokens.FindAsync([queryId], cancellationToken).ConfigureAwait(false);
57+
if (deltaToken is null)
58+
{
59+
_ = context.DatasyncDeltaTokens.Add(new DatasyncDeltaToken() { Id = queryId, Value = unixms });
60+
}
61+
else if (deltaToken.Value != unixms)
62+
{
63+
deltaToken.Value = unixms;
64+
_ = context.DatasyncDeltaTokens.Update(deltaToken);
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Checks to see if the provided queryId is valid.
70+
/// </summary>
71+
/// <param name="queryId">The queryId to check.</param>
72+
/// <exception cref="ArgumentException">Thrown if the queryId is not valid.</exception>
73+
private static void ValidateQueryId(string queryId)
74+
{
75+
if (!EntityResolver.EntityIdIsValid(queryId))
76+
{
77+
throw new ArgumentException("Provided QueryId is not valid", nameof(queryId));
78+
}
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 delta token store holds the last sync times for a specific query.
9+
/// </summary>
10+
internal interface IDeltaTokenStore
11+
{
12+
/// <summary>
13+
/// Obtains the current delta token for a queryId from persistent store.
14+
/// </summary>
15+
/// <param name="queryId">The query ID of the table.</param>
16+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
17+
/// <returns>A task that returns the delta token when complete.</returns>
18+
Task<DateTimeOffset> GetDeltaTokenAsync(string queryId, CancellationToken cancellationToken = default);
19+
20+
/// <summary>
21+
/// Resets the delta token for a queryId from persistent store.
22+
/// </summary>
23+
/// <param name="queryId">The query ID of the table.</param>
24+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
25+
/// <returns>A task that completes when the delta token has been reset.</returns>
26+
Task ResetDeltaTokenAsync(string queryId, CancellationToken cancellationToken = default);
27+
28+
/// <summary>
29+
/// Sets the delta token for a queryId from persistent store.
30+
/// </summary>
31+
/// <param name="queryId">The query ID of the table.</param>
32+
/// <param name="value">The value of the delta token.</param>
33+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
34+
/// <returns>A task that completes when the delta token has been set in the persistent store.</returns>
35+
Task SetDeltaTokenAsync(string queryId, DateTimeOffset value, CancellationToken cancellationToken = default);
36+
}

0 commit comments

Comments
 (0)