-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added offline queue management (#23)
* Offline DbContext and operations queue injection code. * (#20) Tests for updating queue when the DbContext is changed.
- Loading branch information
1 parent
a2f582c
commit 93946b5
Showing
14 changed files
with
1,529 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
src/CommunityToolkit.Datasync.Client/Offline/DatasyncTableInformation.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// 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.ComponentModel.DataAnnotations.Schema; | ||
|
||
namespace CommunityToolkit.Datasync.Client.Offline; | ||
|
||
/// <summary> | ||
/// To support incremental sync, every synchronization event is recorded in the <see cref="OfflineDbContext"/> | ||
/// together with the last time that the synchronization happened. This information is used to determine what | ||
/// parameters to add to the query to get only the changes since the last sync. | ||
/// </summary> | ||
[Table("__datasync_tableinfo")] | ||
public sealed class DatasyncTableInformation | ||
{ | ||
/// <summary> | ||
/// The unique ID for the query. This is generated from the table name | ||
/// and query when not provided. | ||
/// </summary> | ||
[Key] | ||
public string QueryId { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// The number of ticks when the last synchronization happened (or 0L if never synchronized). | ||
/// </summary> | ||
[Required] | ||
public long LastSynchronization { get; set; } = 0L; | ||
} |
38 changes: 38 additions & 0 deletions
38
src/CommunityToolkit.Datasync.Client/Offline/IOperationsQueueManager.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// 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 Microsoft.EntityFrameworkCore.ChangeTracking; | ||
using System.Text.Json; | ||
|
||
namespace CommunityToolkit.Datasync.Client.Offline; | ||
|
||
/// <summary> | ||
/// Definition of the operations queue manager. This is the set of methods used to | ||
/// maintain the queue of operations that need to be synchronized with the remote service. | ||
/// </summary> | ||
internal interface IOperationsQueueManager | ||
{ | ||
/// <summary> | ||
/// The <see cref="JsonSerializerOptions"/> to use for serializing and deserializing data. | ||
/// </summary> | ||
JsonSerializerOptions JsonSerializerOptions { get; } | ||
|
||
/// <summary> | ||
/// Creates a new Create/Insert/Add operation for the given state change. | ||
/// </summary> | ||
/// <param name="entry">The entry being processed.</param> | ||
void AddCreateOperation(EntityEntry entry); | ||
|
||
/// <summary> | ||
/// Creates a new Delete operation for the given state change. | ||
/// </summary> | ||
/// <param name="entry">The entry being processed.</param> | ||
void AddDeleteOperation(EntityEntry entry); | ||
|
||
/// <summary> | ||
/// Creates a new Modify/Update operation for the given state change. | ||
/// </summary> | ||
/// <param name="entry">The entry being processed.</param> | ||
void AddUpdateOperation(EntityEntry entry); | ||
} |
31 changes: 31 additions & 0 deletions
31
src/CommunityToolkit.Datasync.Client/Offline/OfflineClientEntity.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// 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; | ||
|
||
/// <summary> | ||
/// The base class for entities that are stored in an offline database. | ||
/// </summary> | ||
public abstract class OfflineClientEntity | ||
{ | ||
/// <summary> | ||
/// The globally unique ID for the entity. | ||
/// </summary> | ||
public string Id { get; set; } | ||
|
||
/// <summary> | ||
/// The date/time that the entity was last updated on the server. | ||
/// </summary> | ||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.MinValue; | ||
|
||
/// <summary> | ||
/// The version of the entity on the server. | ||
/// </summary> | ||
public string Version { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// If <c>true</c>, the entity is deleted on the server. | ||
/// </summary> | ||
public bool Deleted { get; set; } | ||
} |
29 changes: 29 additions & 0 deletions
29
src/CommunityToolkit.Datasync.Client/Offline/OfflineDatasetException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// 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.Diagnostics.CodeAnalysis; | ||
|
||
namespace CommunityToolkit.Datasync.Client; | ||
|
||
/// <summary> | ||
/// A base exception class for all the exceptions thrown by the offline dataset processing. | ||
/// </summary> | ||
[ExcludeFromCodeCoverage(Justification = "Standard exception class")] | ||
public class OfflineDatasetException : ApplicationException | ||
{ | ||
/// <inheritdoc /> | ||
public OfflineDatasetException() : base() | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
public OfflineDatasetException(string message) : base(message) | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
public OfflineDatasetException(string message, Exception innerException) : base(message, innerException) | ||
{ | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
src/CommunityToolkit.Datasync.Client/Offline/OfflineDbContext.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// 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.Offline; | ||
using CommunityToolkit.Datasync.Server; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.ChangeTracking; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
|
||
namespace CommunityToolkit.Datasync.Client; | ||
|
||
/// <summary> | ||
/// The <see cref="OfflineDbContext"/> class is a base class for a <see cref="DbContext"/> that is | ||
/// used to store offline data that is later synchronized to a remote datasync service. | ||
/// </summary> | ||
public abstract class OfflineDbContext : DbContext | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="OfflineDbContext"/> class. The <see cref="OnConfiguring(DbContextOptionsBuilder)"/> | ||
/// method will be called to configure the database (and other options) to be used for this context. | ||
/// </summary> | ||
[ExcludeFromCodeCoverage(Justification = "Standard entry point that is part of DbContext")] | ||
protected OfflineDbContext() : base() | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="OfflineDbContext"/> class. The <see cref="OnConfiguring(DbContextOptionsBuilder)"/> | ||
/// method will still be called to configure the database to be used for this context. | ||
/// </summary> | ||
/// <param name="options">The options to use in configuring the <see cref="OfflineDbContext"/>.</param> | ||
protected OfflineDbContext(DbContextOptions options) : base(options) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// The list of entities that are awaiting synchronization. | ||
/// </summary> | ||
public DbSet<OfflineQueueEntity> DatasyncOperationsQueue => Set<OfflineQueueEntity>(); | ||
|
||
/// <summary> | ||
/// The metadata storage for each table synchronization set. | ||
/// </summary> | ||
public DbSet<DatasyncTableInformation> DatasyncTableMetadata => Set<DatasyncTableInformation>(); | ||
|
||
/// <summary> | ||
/// The <see cref="JsonSerializerOptions"/> to use for serializing and deserializing entities. | ||
/// </summary> | ||
public JsonSerializerOptions JsonSerializerOptions { get; set; } | ||
|
||
/// <summary> | ||
/// Used to manage the operations queue internally. | ||
/// </summary> | ||
internal IOperationsQueueManager OperationsQueueManager { get; set; } | ||
|
||
/// <inheritdoc /> | ||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||
{ | ||
base.OnConfiguring(optionsBuilder); | ||
JsonSerializerOptions ??= new DatasyncServiceOptions().JsonSerializerOptions; | ||
OperationsQueueManager ??= new OperationsQueueManager(this); | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override void OnModelCreating(ModelBuilder modelBuilder) | ||
{ | ||
modelBuilder.Entity<OfflineQueueEntity>(entity => | ||
{ | ||
entity.HasKey(e => e.Id); | ||
entity.HasIndex(e => e.EntityName); | ||
entity.HasIndex(e => e.EntityId); | ||
}); | ||
|
||
modelBuilder.Entity<DatasyncTableInformation>(entity => | ||
{ | ||
entity.HasKey(e => e.QueryId); | ||
entity.Property(e => e.LastSynchronization).IsRequired().HasDefaultValue(0L); | ||
}); | ||
|
||
base.OnModelCreating(modelBuilder); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public virtual new int SaveChanges() => SaveChanges(true); | ||
|
||
/// <inheritdoc /> | ||
public virtual new int SaveChanges(bool acceptAllChangesOnSuccess) | ||
{ | ||
StoreChangesInOperationsQueue(); | ||
return base.SaveChanges(acceptAllChangesOnSuccess); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public virtual new Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) => SaveChangesAsync(true, cancellationToken); | ||
|
||
/// <inheritdoc /> | ||
public virtual new Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) | ||
{ | ||
StoreChangesInOperationsQueue(); | ||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); | ||
} | ||
|
||
/// <summary> | ||
/// When <see cref="SaveChanges(bool)"/> or the async equivalent is called, this method looks through the changes | ||
/// to see if any should be pushed to the service. An <see cref="OfflineQueueEntity"/> is created for each change. | ||
/// </summary> | ||
/// <remarks> | ||
/// Every offline-capable entity must be derived from OfflineClientData and have a <see cref="DbSet{TEntity}"/> that | ||
/// is using the entity. | ||
/// </remarks> | ||
internal void StoreChangesInOperationsQueue() | ||
{ | ||
ChangeTracker.DetectChanges(); | ||
List<EntityEntry> changedEntities = ChangeTracker.Entries().Where(t => t.State is EntityState.Added or EntityState.Deleted or EntityState.Modified).ToList(); | ||
foreach (EntityEntry entry in changedEntities) | ||
{ | ||
StoreChangeInOperationsQueue(entry); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// When <see cref="SaveChanges(bool)"/> or the async equivalent is called, this method is used to add the entry via | ||
/// the operations queue manager. An <see cref="OfflineQueueEntity"/> is created for each change. | ||
/// </summary> | ||
/// <remarks> | ||
/// Every offline-capable entity must be derived from OfflineClientData and have a <see cref="DbSet{TEntity}"/> that | ||
/// is using the entity. | ||
/// </remarks> | ||
internal void StoreChangeInOperationsQueue(EntityEntry entry) | ||
{ | ||
if (entry.Entity is OfflineClientEntity) | ||
{ | ||
switch (entry.State) | ||
{ | ||
case EntityState.Added: | ||
OperationsQueueManager.AddCreateOperation(entry); | ||
break; | ||
case EntityState.Deleted: | ||
OperationsQueueManager.AddDeleteOperation(entry); | ||
break; | ||
case EntityState.Modified: | ||
OperationsQueueManager.AddUpdateOperation(entry); | ||
break; | ||
default: | ||
throw new InvalidOperationException("Unknown entity state"); | ||
} | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
src/CommunityToolkit.Datasync.Client/Offline/OfflineQueueEntity.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// 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 Microsoft.EntityFrameworkCore; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.ComponentModel.DataAnnotations.Schema; | ||
|
||
namespace CommunityToolkit.Datasync.Client.Offline; | ||
|
||
/// <summary> | ||
/// The potential change types that can be recorded in the <see cref="OfflineQueueEntity"/>. | ||
/// </summary> | ||
public enum EntityChangeType | ||
{ | ||
/// <summary> | ||
/// The change type is unknown (and hence skipped - this is always an error). | ||
/// </summary> | ||
Unknown, | ||
|
||
/// <summary> | ||
/// The entity is being added. | ||
/// </summary> | ||
Add, | ||
|
||
/// <summary> | ||
/// The entity is to be deleted. | ||
/// </summary> | ||
Delete, | ||
|
||
/// <summary> | ||
/// The entity is being updated. | ||
/// </summary> | ||
Update | ||
} | ||
|
||
/// <summary> | ||
/// When an entity is modified while offline, the changes are stored in the <see cref="OfflineDbContext"/> | ||
/// in an operations queue. This entity is used to record what changes are pending push to the server. | ||
/// </summary> | ||
[Table("__datasync_opsqueue")] | ||
[Index(nameof(EntityName))] | ||
public sealed class OfflineQueueEntity | ||
{ | ||
/// <summary> | ||
/// The globally unique ID for the change. | ||
/// </summary> | ||
[Key] | ||
public string Id { get; set; } = Guid.NewGuid().ToString("N"); | ||
|
||
/// <summary> | ||
/// The type of the change (Add, Delete, or Update). | ||
/// </summary> | ||
public EntityChangeType ChangeType { get; set; } = EntityChangeType.Unknown; | ||
|
||
/// <summary> | ||
/// The name of the entity or table that is being modified. | ||
/// </summary> | ||
public string EntityName { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// The ID of the entity that is being modified. | ||
/// </summary> | ||
public string EntityId { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// The replacement version of the entity that is being modified. | ||
/// </summary> | ||
public string ReplacementJsonEntityData { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// The original version of the entity that is being modified. | ||
/// </summary> | ||
public string OriginalJsonEntityEntity { get; set; } = string.Empty; | ||
} |
Oops, something went wrong.