Skip to content

Commit 7c12d6d

Browse files
authored
(#37) Push synchronization logic. (#67)
* Added PushAsync() and push logic to the OfflineDbContext
1 parent f47bcf1 commit 7c12d6d

37 files changed

+3121
-76
lines changed

src/CommunityToolkit.Datasync.Client/CommunityToolkit.Datasync.Client.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<Description>The client capabilities for developing applications using the Datasync Toolkit.</Description>
44
</PropertyGroup>
@@ -13,5 +13,6 @@
1313
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
1414
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
1515
<PackageReference Include="Microsoft.Azure.Core.Spatial" Version="1.1.0" />
16+
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
1617
</ItemGroup>
1718
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
using System.Diagnostics.CodeAnalysis;
7+
8+
namespace CommunityToolkit.Datasync.Client;
9+
10+
/// <summary>
11+
/// An <see cref="ArgumentException"/> that throws on a data validation exception.
12+
/// </summary>
13+
public class ArgumentValidationException : ArgumentException
14+
{
15+
/// <inheritdocs />
16+
[ExcludeFromCodeCoverage]
17+
public ArgumentValidationException() : base() { }
18+
19+
/// <inheritdocs />
20+
[ExcludeFromCodeCoverage]
21+
public ArgumentValidationException(string? message) : base(message) { }
22+
23+
/// <inheritdocs />
24+
[ExcludeFromCodeCoverage]
25+
public ArgumentValidationException(string? message, Exception? innerException) : base(message, innerException) { }
26+
27+
/// <inheritdocs />
28+
[ExcludeFromCodeCoverage]
29+
public ArgumentValidationException(string? message, string? paramName) : base(message, paramName) { }
30+
31+
/// <inheritdocs />
32+
[ExcludeFromCodeCoverage]
33+
public ArgumentValidationException(string? message, string? paramName, Exception? innerException) : base(message, paramName, innerException) { }
34+
35+
/// <summary>
36+
/// The list of validation errors.
37+
/// </summary>
38+
public IList<ValidationResult>? ValidationErrors { get; private set; }
39+
40+
/// <summary>
41+
/// Throws an exception if the object is not valid according to data annotations.
42+
/// </summary>
43+
/// <param name="value">The value being tested.</param>
44+
/// <param name="paramName">The name of the parameter.</param>
45+
public static void ThrowIfNotValid(object? value, string? paramName)
46+
=> ThrowIfNotValid(value, paramName, "Object is not valid");
47+
48+
/// <summary>
49+
/// Throws an exception if the object is not valid according to data annotations.
50+
/// </summary>
51+
/// <param name="value">The value being tested.</param>
52+
/// <param name="paramName">The name of the parameter.</param>
53+
/// <param name="message">The message for the object.</param>
54+
public static void ThrowIfNotValid(object? value, string? paramName, string? message)
55+
{
56+
ArgumentNullException.ThrowIfNull(value, paramName);
57+
List<ValidationResult> results = [];
58+
if (!Validator.TryValidateObject(value, new ValidationContext(value), results, validateAllProperties: true))
59+
{
60+
throw new ArgumentValidationException(message, paramName) { ValidationErrors = results };
61+
}
62+
}
63+
}

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

Lines changed: 0 additions & 2 deletions
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>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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.Http;
6+
7+
/// <summary>
8+
/// A <see cref="IHttpClientFactory"/> implementation that always
9+
/// returns the same <see cref="HttpClient"/>.
10+
/// </summary>
11+
/// <param name="client">The <see cref="HttpClient"/> to use</param>
12+
internal class BasicHttpClientFactory(HttpClient client) : IHttpClientFactory
13+
{
14+
/// <inheritdoc />
15+
public HttpClient CreateClient(string name) => client;
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 options to use for offline operations.
9+
/// </summary>
10+
internal class DatasyncOfflineOptions(HttpClient client, Uri endpoint)
11+
{
12+
/// <summary>
13+
/// The <see cref="HttpClient"/> to use for this request.
14+
/// </summary>
15+
public HttpClient HttpClient { get; } = client;
16+
17+
/// <summary>
18+
/// The relative or absolute URI to the endpoint for this request.
19+
/// </summary>
20+
public Uri Endpoint { get; } = endpoint;
21+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.Http;
6+
7+
namespace CommunityToolkit.Datasync.Client.Offline;
8+
9+
/// <summary>
10+
/// The options builder for the offline operations.
11+
/// </summary>
12+
public class DatasyncOfflineOptionsBuilder
13+
{
14+
internal IHttpClientFactory? _httpClientFactory;
15+
internal readonly Dictionary<string, EntityOfflineOptions> _entities;
16+
17+
/// <summary>
18+
/// Creates the builder based on the required entity types.
19+
/// </summary>
20+
/// <param name="entityTypes">The entity type list.</param>
21+
internal DatasyncOfflineOptionsBuilder(IEnumerable<Type> entityTypes)
22+
{
23+
this._entities = entityTypes.ToDictionary(x => x.FullName!, x => new EntityOfflineOptions(x));
24+
}
25+
26+
/// <summary>
27+
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be
28+
/// the specified <see cref="IHttpClientFactory"/>.
29+
/// </summary>
30+
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> to use.</param>
31+
/// <returns>The current builder for chaining.</returns>
32+
public DatasyncOfflineOptionsBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory)
33+
{
34+
ArgumentNullException.ThrowIfNull(httpClientFactory, nameof(httpClientFactory));
35+
this._httpClientFactory = httpClientFactory;
36+
return this;
37+
}
38+
39+
/// <summary>
40+
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be
41+
/// a constant <see cref="HttpClient"/>
42+
/// </summary>
43+
/// <param name="httpClient">The <see cref="HttpClient"/> to use.</param>
44+
/// <returns>The current builder for chaining.</returns>
45+
public DatasyncOfflineOptionsBuilder UseHttpClient(HttpClient httpClient)
46+
{
47+
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
48+
this._httpClientFactory = new BasicHttpClientFactory(httpClient);
49+
return this;
50+
}
51+
52+
/// <summary>
53+
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be a
54+
/// standard <see cref="IHttpClientFactory"/> based on the provided endpoint.
55+
/// </summary>
56+
/// <param name="endpoint">The <see cref="Uri"/> pointing to the datasync endpoint.</param>
57+
/// <returns>The current builder for chaining.</returns>
58+
public DatasyncOfflineOptionsBuilder UseEndpoint(Uri endpoint)
59+
{
60+
ArgumentNullException.ThrowIfNull(endpoint, nameof(endpoint));
61+
ThrowIf.IsNotValidEndpoint(endpoint, nameof(endpoint));
62+
this._httpClientFactory = new HttpClientFactory(new HttpClientOptions { Endpoint = endpoint });
63+
return this;
64+
}
65+
66+
/// <summary>
67+
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be a
68+
/// standard <see cref="IHttpClientFactory"/> based on the provided client options
69+
/// </summary>
70+
/// <param name="clientOptions">The <see cref="HttpClientOptions"/> pointing to the datasync endpoint.</param>
71+
/// <returns>The current builder for chaining.</returns>
72+
public DatasyncOfflineOptionsBuilder UseHttpClientOptions(HttpClientOptions clientOptions)
73+
{
74+
ArgumentNullException.ThrowIfNull(clientOptions, nameof(clientOptions));
75+
this._httpClientFactory = new HttpClientFactory(clientOptions);
76+
return this;
77+
}
78+
79+
/// <summary>
80+
/// Configures the specified entity type for offline operations.
81+
/// </summary>
82+
/// <typeparam name="TEntity">The type of the entity.</typeparam>
83+
/// <param name="configure">A configuration function for the entity.</param>
84+
/// <returns>The current builder for chaining.</returns>
85+
public DatasyncOfflineOptionsBuilder Entity<TEntity>(Action<EntityOfflineOptions> configure)
86+
=> Entity(typeof(TEntity), configure);
87+
88+
/// <summary>
89+
/// Configures the specified entity type for offline operations.
90+
/// </summary>
91+
/// <param name="entityType">The type of the entity.</param>
92+
/// <param name="configure">A configuration function for the entity.</param>
93+
/// <returns>The current builder for chaining.</returns>
94+
public DatasyncOfflineOptionsBuilder Entity(Type entityType, Action<EntityOfflineOptions> configure)
95+
{
96+
ArgumentNullException.ThrowIfNull(entityType, nameof(entityType));
97+
ArgumentNullException.ThrowIfNull(configure, nameof(configure));
98+
if (!this._entities.TryGetValue(entityType.FullName!, out EntityOfflineOptions? options))
99+
{
100+
throw new DatasyncException($"Entity is not synchronizable.");
101+
}
102+
103+
configure(options);
104+
return this;
105+
}
106+
107+
/// <summary>
108+
/// Retrieves the offline options for the entity type.
109+
/// </summary>
110+
/// <param name="entityType">The entity type.</param>
111+
/// <returns>The offline options for the entity type.</returns>
112+
internal DatasyncOfflineOptions GetOfflineOptions(Type entityType)
113+
{
114+
ArgumentNullException.ThrowIfNull(entityType, nameof(entityType));
115+
if (this._httpClientFactory == null)
116+
{
117+
throw new DatasyncException($"Datasync service connection is not set.");
118+
}
119+
120+
if (!this._entities.TryGetValue(entityType.FullName!, out EntityOfflineOptions? options))
121+
{
122+
throw new DatasyncException($"Entity is not synchronizable.");
123+
}
124+
125+
HttpClient client = this._httpClientFactory.CreateClient(options.ClientName);
126+
return new DatasyncOfflineOptions(client, options.Endpoint);
127+
}
128+
129+
/// <summary>
130+
/// The entity offline options that are used for configuration.
131+
/// </summary>
132+
/// <param name="entityType">The entity type for this entity offline options.</param>
133+
public class EntityOfflineOptions(Type entityType)
134+
{
135+
/// <summary>
136+
/// The entity type being configured.
137+
/// </summary>
138+
public Type EntityType { get => entityType; }
139+
140+
/// <summary>
141+
/// The endpoint for the entity type.
142+
/// </summary>
143+
public Uri Endpoint { get; set; } = new Uri($"/tables/{entityType.Name.ToLowerInvariant()}", UriKind.Relative);
144+
145+
/// <summary>
146+
/// The name of the client to use when requesting a <see cref="HttpClient"/>.
147+
/// </summary>
148+
public string ClientName { get; set; } = string.Empty;
149+
}
150+
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
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 Microsoft.EntityFrameworkCore;
56
using Microsoft.EntityFrameworkCore.Metadata.Internal;
7+
using System.ComponentModel;
8+
using System.ComponentModel.DataAnnotations;
69

710
namespace CommunityToolkit.Datasync.Client.Offline;
811

@@ -35,47 +38,72 @@ public enum OperationState
3538
/// <summary>
3639
/// An entity representing a pending operation against an entity set.
3740
/// </summary>
41+
[Index(nameof(ItemId), nameof(EntityType))]
3842
public class DatasyncOperation
3943
{
4044
/// <summary>
4145
/// A unique ID for the operation.
4246
/// </summary>
47+
[Key]
4348
public required string Id { get; set; }
4449

4550
/// <summary>
4651
/// The kind of operation that this entity represents.
4752
/// </summary>
53+
[Required]
4854
public required OperationKind Kind { get; set; }
4955

5056
/// <summary>
5157
/// The current state of the operation.
5258
/// </summary>
59+
[Required]
5360
public required OperationState State { get; set; }
5461

62+
/// <summary>
63+
/// The date/time of the last attempt.
64+
/// </summary>
65+
public DateTimeOffset? LastAttempt { get; set; }
66+
67+
/// <summary>
68+
/// The HTTP Status Code for the last attempt.
69+
/// </summary>
70+
public int? HttpStatusCode { get; set; }
71+
5572
/// <summary>
5673
/// The fully qualified name of the entity type.
5774
/// </summary>
75+
[Required, MaxLength(255)]
5876
public required string EntityType { get; set; }
5977

6078
/// <summary>
6179
/// The globally unique ID of the entity.
6280
/// </summary>
81+
[Required, MaxLength(126)]
6382
public required string ItemId { get; set; }
6483

84+
/// <summary>
85+
/// The version of the entity currently downloaded from the service.
86+
/// </summary>
87+
[Required, MaxLength(126)]
88+
public required string EntityVersion { get; set; }
89+
6590
/// <summary>
6691
/// The JSON-encoded representation of the Item.
6792
/// </summary>
93+
[Required, DataType(DataType.Text)]
6894
public required string Item { get; set; }
6995

7096
/// <summary>
7197
/// The sequence number for the operation. This is incremented for each
7298
/// new operation to a different entity.
7399
/// </summary>
100+
[DefaultValue(0L)]
74101
public required long Sequence { get; set; }
75102

76103
/// <summary>
77104
/// The version number for the operation. This is incremented as multiple
78105
/// changes to the same entity are performed in between pushes.
79106
/// </summary>
107+
[DefaultValue(0)]
80108
public required int Version { get; set; }
81109
}

0 commit comments

Comments
 (0)