Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(#37) Push synchronization logic. #67

Merged
merged 6 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>The client capabilities for developing applications using the Datasync Toolkit.</Description>
</PropertyGroup>
Expand All @@ -13,5 +13,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Azure.Core.Spatial" Version="1.1.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;

namespace CommunityToolkit.Datasync.Client;

/// <summary>
/// An <see cref="ArgumentException"/> that throws on a data validation exception.
/// </summary>
public class ArgumentValidationException : ArgumentException
{
/// <inheritdocs />
[ExcludeFromCodeCoverage]
public ArgumentValidationException() : base() { }

/// <inheritdocs />
[ExcludeFromCodeCoverage]
public ArgumentValidationException(string? message) : base(message) { }

/// <inheritdocs />
[ExcludeFromCodeCoverage]
public ArgumentValidationException(string? message, Exception? innerException) : base(message, innerException) { }

/// <inheritdocs />
[ExcludeFromCodeCoverage]
public ArgumentValidationException(string? message, string? paramName) : base(message, paramName) { }

/// <inheritdocs />
[ExcludeFromCodeCoverage]
public ArgumentValidationException(string? message, string? paramName, Exception? innerException) : base(message, paramName, innerException) { }

/// <summary>
/// The list of validation errors.
/// </summary>
public IList<ValidationResult>? ValidationErrors { get; private set; }

/// <summary>
/// Throws an exception if the object is not valid according to data annotations.
/// </summary>
/// <param name="value">The value being tested.</param>
/// <param name="paramName">The name of the parameter.</param>
public static void ThrowIfNotValid(object? value, string? paramName)
=> ThrowIfNotValid(value, paramName, "Object is not valid");

/// <summary>
/// Throws an exception if the object is not valid according to data annotations.
/// </summary>
/// <param name="value">The value being tested.</param>
/// <param name="paramName">The name of the parameter.</param>
/// <param name="message">The message for the object.</param>
public static void ThrowIfNotValid(object? value, string? paramName, string? message)
{
ArgumentNullException.ThrowIfNull(value, paramName);
List<ValidationResult> results = [];
if (!Validator.TryValidateObject(value, new ValidationContext(value), results, validateAllProperties: true))
{
throw new ArgumentValidationException(message, paramName) { ValidationErrors = results };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// 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.Client.Service;

namespace CommunityToolkit.Datasync.Client;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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.

namespace CommunityToolkit.Datasync.Client.Http;

/// <summary>
/// A <see cref="IHttpClientFactory"/> implementation that always
/// returns the same <see cref="HttpClient"/>.
/// </summary>
/// <param name="client">The <see cref="HttpClient"/> to use</param>
internal class BasicHttpClientFactory(HttpClient client) : IHttpClientFactory
{
/// <inheritdoc />
public HttpClient CreateClient(string name) => client;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.

namespace CommunityToolkit.Datasync.Client.Offline;

/// <summary>
/// The options to use for offline operations.
/// </summary>
internal class DatasyncOfflineOptions(HttpClient client, Uri endpoint)
{
/// <summary>
/// The <see cref="HttpClient"/> to use for this request.
/// </summary>
public HttpClient HttpClient { get; } = client;

/// <summary>
/// The relative or absolute URI to the endpoint for this request.
/// </summary>
public Uri Endpoint { get; } = endpoint;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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.Client.Http;

namespace CommunityToolkit.Datasync.Client.Offline;

/// <summary>
/// The options builder for the offline operations.
/// </summary>
public class DatasyncOfflineOptionsBuilder
{
internal IHttpClientFactory? _httpClientFactory;
internal readonly Dictionary<string, EntityOfflineOptions> _entities;

/// <summary>
/// Creates the builder based on the required entity types.
/// </summary>
/// <param name="entityTypes">The entity type list.</param>
internal DatasyncOfflineOptionsBuilder(IEnumerable<Type> entityTypes)
{
this._entities = entityTypes.ToDictionary(x => x.FullName!, x => new EntityOfflineOptions(x));
}

/// <summary>
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be
/// the specified <see cref="IHttpClientFactory"/>.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> to use.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory)
{
ArgumentNullException.ThrowIfNull(httpClientFactory, nameof(httpClientFactory));
this._httpClientFactory = httpClientFactory;
return this;
}

/// <summary>
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be
/// a constant <see cref="HttpClient"/>
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to use.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder UseHttpClient(HttpClient httpClient)
{
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
this._httpClientFactory = new BasicHttpClientFactory(httpClient);
return this;
}

/// <summary>
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be a
/// standard <see cref="IHttpClientFactory"/> based on the provided endpoint.
/// </summary>
/// <param name="endpoint">The <see cref="Uri"/> pointing to the datasync endpoint.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder UseEndpoint(Uri endpoint)
{
ArgumentNullException.ThrowIfNull(endpoint, nameof(endpoint));
ThrowIf.IsNotValidEndpoint(endpoint, nameof(endpoint));
this._httpClientFactory = new HttpClientFactory(new HttpClientOptions { Endpoint = endpoint });
return this;
}

/// <summary>
/// Sets the default mechanism for getting a <see cref="HttpClient"/> to be a
/// standard <see cref="IHttpClientFactory"/> based on the provided client options
/// </summary>
/// <param name="clientOptions">The <see cref="HttpClientOptions"/> pointing to the datasync endpoint.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder UseHttpClientOptions(HttpClientOptions clientOptions)
{
ArgumentNullException.ThrowIfNull(clientOptions, nameof(clientOptions));
this._httpClientFactory = new HttpClientFactory(clientOptions);
return this;
}

/// <summary>
/// Configures the specified entity type for offline operations.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="configure">A configuration function for the entity.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder Entity<TEntity>(Action<EntityOfflineOptions> configure)
=> Entity(typeof(TEntity), configure);

/// <summary>
/// Configures the specified entity type for offline operations.
/// </summary>
/// <param name="entityType">The type of the entity.</param>
/// <param name="configure">A configuration function for the entity.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder Entity(Type entityType, Action<EntityOfflineOptions> configure)
{
ArgumentNullException.ThrowIfNull(entityType, nameof(entityType));
ArgumentNullException.ThrowIfNull(configure, nameof(configure));
if (!this._entities.TryGetValue(entityType.FullName!, out EntityOfflineOptions? options))
{
throw new DatasyncException($"Entity is not synchronizable.");
}

configure(options);
return this;
}

/// <summary>
/// Retrieves the offline options for the entity type.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The offline options for the entity type.</returns>
internal DatasyncOfflineOptions GetOfflineOptions(Type entityType)
{
ArgumentNullException.ThrowIfNull(entityType, nameof(entityType));
if (this._httpClientFactory == null)
{
throw new DatasyncException($"Datasync service connection is not set.");
}

if (!this._entities.TryGetValue(entityType.FullName!, out EntityOfflineOptions? options))
{
throw new DatasyncException($"Entity is not synchronizable.");
}

HttpClient client = this._httpClientFactory.CreateClient(options.ClientName);
return new DatasyncOfflineOptions(client, options.Endpoint);
}

/// <summary>
/// The entity offline options that are used for configuration.
/// </summary>
/// <param name="entityType">The entity type for this entity offline options.</param>
public class EntityOfflineOptions(Type entityType)
{
/// <summary>
/// The entity type being configured.
/// </summary>
public Type EntityType { get => entityType; }

/// <summary>
/// The endpoint for the entity type.
/// </summary>
public Uri Endpoint { get; set; } = new Uri($"/tables/{entityType.Name.ToLowerInvariant()}", UriKind.Relative);

/// <summary>
/// The name of the client to use when requesting a <see cref="HttpClient"/>.
/// </summary>
public string ClientName { get; set; } = string.Empty;
}
}
28 changes: 28 additions & 0 deletions src/CommunityToolkit.Datasync.Client/Offline/DatasyncOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace CommunityToolkit.Datasync.Client.Offline;

Expand Down Expand Up @@ -35,47 +38,72 @@ public enum OperationState
/// <summary>
/// An entity representing a pending operation against an entity set.
/// </summary>
[Index(nameof(ItemId), nameof(EntityType))]
public class DatasyncOperation
{
/// <summary>
/// A unique ID for the operation.
/// </summary>
[Key]
public required string Id { get; set; }

/// <summary>
/// The kind of operation that this entity represents.
/// </summary>
[Required]
public required OperationKind Kind { get; set; }

/// <summary>
/// The current state of the operation.
/// </summary>
[Required]
public required OperationState State { get; set; }

/// <summary>
/// The date/time of the last attempt.
/// </summary>
public DateTimeOffset? LastAttempt { get; set; }

/// <summary>
/// The HTTP Status Code for the last attempt.
/// </summary>
public int? HttpStatusCode { get; set; }

/// <summary>
/// The fully qualified name of the entity type.
/// </summary>
[Required, MaxLength(255)]
public required string EntityType { get; set; }

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

/// <summary>
/// The version of the entity currently downloaded from the service.
/// </summary>
[Required, MaxLength(126)]
public required string EntityVersion { get; set; }

/// <summary>
/// The JSON-encoded representation of the Item.
/// </summary>
[Required, DataType(DataType.Text)]
public required string Item { get; set; }

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

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