Harden Surreal migration with retry/coverage fixes and XML docs cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s

This commit is contained in:
Joseph Doherty
2026-02-22 05:39:00 -05:00
parent 9c2a77dc3c
commit bd10914828
27 changed files with 1402 additions and 19 deletions

View File

@@ -14,6 +14,11 @@ public class SampleDbContext : IDisposable
private readonly bool _ownsClient;
/// <summary>
/// Initializes a new instance of the <see cref="SampleDbContext"/> class.
/// </summary>
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
/// <param name="schemaInitializer">The schema initializer.</param>
public SampleDbContext(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer)
@@ -29,6 +34,10 @@ public class SampleDbContext : IDisposable
SchemaInitializer);
}
/// <summary>
/// Initializes a new instance of the <see cref="SampleDbContext"/> class.
/// </summary>
/// <param name="databasePath">The database path used for the embedded store.</param>
public SampleDbContext(string databasePath)
{
string normalizedPath = NormalizeDatabasePath(databasePath);
@@ -54,21 +63,41 @@ public class SampleDbContext : IDisposable
SchemaInitializer);
}
/// <summary>
/// Gets the embedded SurrealDB client.
/// </summary>
public ICBDDCSurrealEmbeddedClient SurrealEmbeddedClient { get; }
/// <summary>
/// Gets the schema initializer.
/// </summary>
public ICBDDCSurrealSchemaInitializer SchemaInitializer { get; private set; }
/// <summary>
/// Gets the users collection.
/// </summary>
public SampleSurrealCollection<User> Users { get; private set; }
/// <summary>
/// Gets the todo lists collection.
/// </summary>
public SampleSurrealCollection<TodoList> TodoLists { get; private set; }
/// <summary>
/// Gets the operation log entries collection.
/// </summary>
public SampleSurrealReadOnlyCollection<SampleOplogEntry> OplogEntries { get; private set; }
/// <summary>
/// Ensures schema changes are applied before persisting updates.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
await SchemaInitializer.EnsureInitializedAsync(cancellationToken);
}
/// <inheritdoc />
public void Dispose()
{
Users.Dispose();
@@ -101,11 +130,16 @@ public sealed class SampleSurrealSchemaInitializer : ICBDDCSurrealSchemaInitiali
private readonly ICBDDCSurrealEmbeddedClient _client;
private int _initialized;
/// <summary>
/// Initializes a new instance of the <see cref="SampleSurrealSchemaInitializer"/> class.
/// </summary>
/// <param name="client">The embedded SurrealDB client.</param>
public SampleSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <inheritdoc />
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
if (Volatile.Read(ref _initialized) == 1) return;
@@ -124,6 +158,13 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
private readonly string _tableName;
/// <summary>
/// Initializes a new instance of the <see cref="SampleSurrealCollection{TEntity}"/> class.
/// </summary>
/// <param name="tableName">The backing table name.</param>
/// <param name="keySelector">The key selector for entities.</param>
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
/// <param name="schemaInitializer">The schema initializer.</param>
public SampleSurrealCollection(
string tableName,
Func<TEntity, string> keySelector,
@@ -139,21 +180,25 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
}
/// <inheritdoc />
public IDisposable Subscribe(IObserver<SurrealCollectionChange<TEntity>> observer)
{
return _changeFeed.Subscribe(observer);
}
/// <inheritdoc />
public async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await UpsertAsync(entity, cancellationToken);
}
/// <inheritdoc />
public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await UpsertAsync(entity, cancellationToken);
}
/// <inheritdoc />
public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id))
@@ -164,11 +209,22 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
_changeFeed.PublishDelete(id);
}
/// <summary>
/// Finds an entity by identifier.
/// </summary>
/// <param name="id">The entity identifier.</param>
/// <returns>The matching entity when found; otherwise <see langword="null"/>.</returns>
public TEntity? FindById(string id)
{
return FindByIdAsync(id).GetAwaiter().GetResult();
}
/// <summary>
/// Finds an entity by identifier asynchronously.
/// </summary>
/// <param name="id">The entity identifier.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The matching entity when found; otherwise <see langword="null"/>.</returns>
public async Task<TEntity?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id))
@@ -179,11 +235,13 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
return record?.Entity;
}
/// <inheritdoc />
public IEnumerable<TEntity> FindAll()
{
return FindAllAsync().GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
@@ -195,12 +253,14 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
?? [];
}
/// <inheritdoc />
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
return FindAll().Where(predicate);
}
/// <inheritdoc />
public void Dispose()
{
_changeFeed.Dispose();
@@ -235,6 +295,12 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
private readonly string _tableName;
/// <summary>
/// Initializes a new instance of the <see cref="SampleSurrealReadOnlyCollection{TEntity}"/> class.
/// </summary>
/// <param name="tableName">The backing table name.</param>
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
/// <param name="schemaInitializer">The schema initializer.</param>
public SampleSurrealReadOnlyCollection(
string tableName,
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
@@ -248,11 +314,20 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
}
/// <summary>
/// Returns all entities from the collection.
/// </summary>
/// <returns>The entities in the collection.</returns>
public IEnumerable<TEntity> FindAll()
{
return FindAllAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Returns all entities from the collection asynchronously.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The entities in the collection.</returns>
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
{
await _schemaInitializer.EnsureInitializedAsync(cancellationToken);
@@ -260,6 +335,11 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
return rows?.ToList() ?? [];
}
/// <summary>
/// Returns entities that match the provided predicate.
/// </summary>
/// <param name="predicate">The predicate used to filter entities.</param>
/// <returns>The entities that satisfy the predicate.</returns>
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
@@ -270,30 +350,54 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
public sealed class SampleEntityRecord<TEntity> : Record
where TEntity : class
{
/// <summary>
/// Gets or sets the stored entity payload.
/// </summary>
[JsonPropertyName("entity")]
public TEntity? Entity { get; set; }
}
public sealed class SampleOplogEntry : Record
{
/// <summary>
/// Gets or sets the collection name.
/// </summary>
[JsonPropertyName("collection")]
public string Collection { get; set; } = "";
/// <summary>
/// Gets or sets the entity key.
/// </summary>
[JsonPropertyName("key")]
public string Key { get; set; } = "";
/// <summary>
/// Gets or sets the operation code.
/// </summary>
[JsonPropertyName("operation")]
public int Operation { get; set; }
/// <summary>
/// Gets or sets the node identifier portion of the timestamp.
/// </summary>
[JsonPropertyName("timestampNodeId")]
public string TimestampNodeId { get; set; } = "";
/// <summary>
/// Gets or sets the physical time portion of the timestamp.
/// </summary>
[JsonPropertyName("timestampPhysicalTime")]
public long TimestampPhysicalTime { get; set; }
/// <summary>
/// Gets or sets the logical counter portion of the timestamp.
/// </summary>
[JsonPropertyName("timestampLogicalCounter")]
public int TimestampLogicalCounter { get; set; }
/// <summary>
/// Gets or sets the hash for the operation entry.
/// </summary>
[JsonPropertyName("hash")]
public string Hash { get; set; } = "";
}