Replace BLite with Surreal embedded persistence
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m21s

This commit is contained in:
Joseph Doherty
2026-02-22 05:21:53 -05:00
parent 7ebc2cb567
commit 9c2a77dc3c
56 changed files with 6613 additions and 3177 deletions

View File

@@ -3,10 +3,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Text.Json;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Network;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using ZB.MOM.WW.CBDDC.Persistence.Snapshot;
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
namespace ZB.MOM.WW.CBDDC.Sample.Console;
@@ -16,6 +19,8 @@ internal class Program
{
private static async Task Main(string[] args)
{
if (await TryRunMigrationAsync(args)) return;
var builder = Host.CreateApplicationBuilder(args);
// Configuration
@@ -55,11 +60,20 @@ internal class Program
// Database path
string dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data");
Directory.CreateDirectory(dataPath);
string databasePath = Path.Combine(dataPath, $"{nodeId}.blite");
string databasePath = Path.Combine(dataPath, $"{nodeId}.rocksdb");
string surrealDatabase = nodeId.Replace("-", "_", StringComparison.Ordinal);
// Register CBDDC Services using Fluent Extensions with BLite, SampleDbContext, and SampleDocumentStore
// Register CBDDC services with embedded Surreal (RocksDB).
builder.Services.AddSingleton<ICBDDCSurrealSchemaInitializer, SampleSurrealSchemaInitializer>();
builder.Services.AddSingleton<SampleDbContext>();
builder.Services.AddCBDDCCore()
.AddCBDDCBLite<SampleDbContext, SampleDocumentStore>(sp => new SampleDbContext(databasePath))
.AddCBDDCSurrealEmbedded<SampleDocumentStore>(_ => new CBDDCSurrealEmbeddedOptions
{
Endpoint = "rocksdb://local",
DatabasePath = databasePath,
Namespace = "cbddc_sample",
Database = surrealDatabase
})
.AddCBDDCNetwork<StaticPeerNodeConfigurationProvider>(); // useHostedService = true by default
builder.Services.AddHostedService<ConsoleInteractiveService>(); // Runs the Input Loop
@@ -73,6 +87,107 @@ internal class Program
await host.RunAsync();
}
private static async Task<bool> TryRunMigrationAsync(string[] args)
{
int migrateIndex = Array.IndexOf(args, "--migrate-snapshot");
if (migrateIndex < 0) return false;
string snapshotPath = GetRequiredArgumentValue(args, migrateIndex, "--migrate-snapshot");
if (!File.Exists(snapshotPath))
throw new FileNotFoundException("Snapshot file not found.", snapshotPath);
string targetPath = GetOptionalArgumentValue(args, "--target-db")
?? Path.Combine(Directory.GetCurrentDirectory(), "data", "migration.rocksdb");
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(targetPath))!);
string nodeId = "migration-node";
var configProvider = new StaticPeerNodeConfigurationProvider(new PeerNodeConfiguration
{
NodeId = nodeId,
TcpPort = 0,
AuthToken = "migration"
});
string databaseName = $"migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IPeerNodeConfigurationProvider>(configProvider);
services.AddSingleton<ICBDDCSurrealSchemaInitializer, SampleSurrealSchemaInitializer>();
services.AddSingleton<SampleDbContext>();
services.AddCBDDCCore()
.AddCBDDCSurrealEmbedded<SampleDocumentStore>(_ => new CBDDCSurrealEmbeddedOptions
{
Endpoint = "rocksdb://local",
DatabasePath = targetPath,
Namespace = "cbddc_migration",
Database = databaseName
});
using var provider = services.BuildServiceProvider();
var snapshotService = provider.GetRequiredService<ISnapshotService>();
await using (var snapshotStream = File.OpenRead(snapshotPath))
{
await snapshotService.ReplaceDatabaseAsync(snapshotStream);
}
await VerifyMigrationAsync(provider, snapshotPath);
System.Console.WriteLine($"Migration completed successfully to: {targetPath}");
return true;
}
private static async Task VerifyMigrationAsync(IServiceProvider provider, string snapshotPath)
{
await using var snapshotStream = File.OpenRead(snapshotPath);
var source = await JsonSerializer.DeserializeAsync<SnapshotDto>(snapshotStream)
?? throw new InvalidOperationException("Unable to deserialize source snapshot.");
var documentStore = provider.GetRequiredService<IDocumentStore>();
var oplogStore = provider.GetRequiredService<IOplogStore>();
var peerStore = provider.GetRequiredService<IPeerConfigurationStore>();
var confirmationStore = provider.GetService<IPeerOplogConfirmationStore>();
int destinationDocuments = (await documentStore.ExportAsync()).Count();
int destinationOplog = (await oplogStore.ExportAsync()).Count();
int destinationPeers = (await peerStore.ExportAsync()).Count();
int destinationConfirmations = confirmationStore == null
? 0
: (await confirmationStore.ExportAsync()).Count();
if (destinationDocuments != source.Documents.Count ||
destinationOplog != source.Oplog.Count ||
destinationPeers != source.RemotePeers.Count ||
destinationConfirmations != source.PeerConfirmations.Count)
throw new InvalidOperationException("Snapshot parity verification failed after migration.");
if (source.Oplog.Count > 0)
{
string firstHash = source.Oplog[0].Hash;
string lastHash = source.Oplog[^1].Hash;
var firstEntry = await oplogStore.GetEntryByHashAsync(firstHash);
var lastEntry = await oplogStore.GetEntryByHashAsync(lastHash);
if (firstEntry == null || lastEntry == null)
throw new InvalidOperationException("Oplog hash spot-check failed after migration.");
}
}
private static string GetRequiredArgumentValue(string[] args, int optionIndex, string optionName)
{
if (optionIndex < 0 || optionIndex + 1 >= args.Length || args[optionIndex + 1].StartsWith("--"))
throw new ArgumentException($"Missing value for {optionName}.");
return args[optionIndex + 1];
}
private static string? GetOptionalArgumentValue(string[] args, string optionName)
{
int index = Array.IndexOf(args, optionName);
if (index < 0) return null;
if (index + 1 >= args.Length || args[index + 1].StartsWith("--"))
throw new ArgumentException($"Missing value for {optionName}.");
return args[index + 1];
}
private class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
{
/// <summary>
@@ -112,4 +227,4 @@ internal class Program
ConfigurationChanged?.Invoke(this, newConfig);
}
}
}
}

View File

@@ -56,7 +56,15 @@ Terminal 3:
dotnet run -- --node-id node3 --tcp-port 5003 --udp-port 6003
```
Changes made on any node will automatically sync to all peers!
Changes made on any node will automatically sync to all peers!
### Import Snapshot Into Surreal (Migration Utility)
```bash
dotnet run -- --migrate-snapshot /path/to/snapshot.json --target-db /path/to/data.rocksdb
```
This imports a CBDDC snapshot into embedded Surreal RocksDB and validates parity (counts plus oplog hash spot checks).
## Available Commands
@@ -149,7 +157,7 @@ var page = await users.Find(u => true, skip: 10, take: 5);
## Architecture
- **Storage**: SQLite with HLC timestamps
- **Storage**: Surreal embedded RocksDB with HLC timestamps
- **Sync**: TCP for data transfer, UDP for discovery
- **Conflict Resolution**: Last-Write-Wins based on Hybrid Logical Clocks
- **Serialization**: System.Text.Json

View File

@@ -1,50 +1,299 @@
using BLite.Core.Collections;
using BLite.Core.Metadata;
using BLite.Core.Storage;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using System.Text.Json.Serialization;
using System.Security.Cryptography;
using System.Text;
using SurrealDb.Net;
using SurrealDb.Net.Models;
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
namespace ZB.MOM.WW.CBDDC.Sample.Console;
public class SampleDbContext : CBDDCDocumentDbContext
public class SampleDbContext : IDisposable
{
/// <summary>
/// Initializes a new instance of the SampleDbContext class using the specified database file path.
/// </summary>
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
public SampleDbContext(string databasePath) : base(databasePath)
private const string UsersTable = "sample_users";
private const string TodoListsTable = "sample_todo_lists";
private readonly bool _ownsClient;
public SampleDbContext(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer)
{
SurrealEmbeddedClient = surrealEmbeddedClient ?? throw new ArgumentNullException(nameof(surrealEmbeddedClient));
SchemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
Users = new SampleSurrealCollection<User>(UsersTable, u => u.Id, SurrealEmbeddedClient, SchemaInitializer);
TodoLists = new SampleSurrealCollection<TodoList>(TodoListsTable, t => t.Id, SurrealEmbeddedClient, SchemaInitializer);
OplogEntries = new SampleSurrealReadOnlyCollection<SampleOplogEntry>(
CBDDCSurrealSchemaNames.OplogEntriesTable,
SurrealEmbeddedClient,
SchemaInitializer);
}
/// <summary>
/// Initializes a new instance of the SampleDbContext class using the specified database file path and page file
/// configuration.
/// </summary>
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
/// <param name="config">The configuration settings for the page file. Cannot be null.</param>
public SampleDbContext(string databasePath, PageFileConfig config) : base(databasePath, config)
public SampleDbContext(string databasePath)
{
string normalizedPath = NormalizeDatabasePath(databasePath);
string suffix = ComputeDeterministicSuffix(normalizedPath);
var options = new CBDDCSurrealEmbeddedOptions
{
Endpoint = "rocksdb://local",
DatabasePath = normalizedPath,
Namespace = $"cbddc_sample_{suffix}",
Database = $"main_{suffix}"
};
SurrealEmbeddedClient = new CBDDCSurrealEmbeddedClient(options);
_ownsClient = true;
SchemaInitializer = new SampleSurrealSchemaInitializer(SurrealEmbeddedClient);
Users = new SampleSurrealCollection<User>(UsersTable, u => u.Id, SurrealEmbeddedClient, SchemaInitializer);
TodoLists = new SampleSurrealCollection<TodoList>(TodoListsTable, t => t.Id, SurrealEmbeddedClient, SchemaInitializer);
OplogEntries = new SampleSurrealReadOnlyCollection<SampleOplogEntry>(
CBDDCSurrealSchemaNames.OplogEntriesTable,
SurrealEmbeddedClient,
SchemaInitializer);
}
/// <summary>
/// Gets the users collection.
/// </summary>
public DocumentCollection<string, User> Users { get; private set; } = null!;
public ICBDDCSurrealEmbeddedClient SurrealEmbeddedClient { get; }
/// <summary>
/// Gets the todo lists collection.
/// </summary>
public DocumentCollection<string, TodoList> TodoLists { get; private set; } = null!;
public ICBDDCSurrealSchemaInitializer SchemaInitializer { get; private set; }
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
public SampleSurrealCollection<User> Users { get; private set; }
public SampleSurrealCollection<TodoList> TodoLists { get; private set; }
public SampleSurrealReadOnlyCollection<SampleOplogEntry> OplogEntries { get; private set; }
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.ToCollection("Users")
.HasKey(u => u.Id);
modelBuilder.Entity<TodoList>()
.ToCollection("TodoLists")
.HasKey(t => t.Id);
await SchemaInitializer.EnsureInitializedAsync(cancellationToken);
}
}
public void Dispose()
{
Users.Dispose();
TodoLists.Dispose();
if (_ownsClient) SurrealEmbeddedClient.Dispose();
}
private static string NormalizeDatabasePath(string databasePath)
{
if (string.IsNullOrWhiteSpace(databasePath))
throw new ArgumentException("Database path is required.", nameof(databasePath));
return Path.GetFullPath(databasePath);
}
private static string ComputeDeterministicSuffix(string value)
{
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value));
return Convert.ToHexString(bytes).ToLowerInvariant()[..12];
}
}
public sealed class SampleSurrealSchemaInitializer : ICBDDCSurrealSchemaInitializer
{
private const string SampleSchemaSql = """
DEFINE TABLE OVERWRITE sample_users SCHEMALESS CHANGEFEED 7d;
DEFINE TABLE OVERWRITE sample_todo_lists SCHEMALESS CHANGEFEED 7d;
""";
private readonly ICBDDCSurrealEmbeddedClient _client;
private int _initialized;
public SampleSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
if (Volatile.Read(ref _initialized) == 1) return;
await _client.InitializeAsync(cancellationToken);
await _client.RawQueryAsync(SampleSchemaSql, cancellationToken: cancellationToken);
Volatile.Write(ref _initialized, 1);
}
}
public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollection<TEntity>, IDisposable
where TEntity : class
{
private readonly SurrealCollectionChangeFeed<TEntity> _changeFeed = new();
private readonly ISurrealDbClient _client;
private readonly Func<TEntity, string> _keySelector;
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
private readonly string _tableName;
public SampleSurrealCollection(
string tableName,
Func<TEntity, string> keySelector,
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer)
{
if (string.IsNullOrWhiteSpace(tableName))
throw new ArgumentException("Table name is required.", nameof(tableName));
_tableName = tableName;
_keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
_client = (surrealEmbeddedClient ?? throw new ArgumentNullException(nameof(surrealEmbeddedClient))).Client;
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
}
public IDisposable Subscribe(IObserver<SurrealCollectionChange<TEntity>> observer)
{
return _changeFeed.Subscribe(observer);
}
public async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await UpsertAsync(entity, cancellationToken);
}
public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await UpsertAsync(entity, cancellationToken);
}
public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Document id is required.", nameof(id));
await EnsureReadyAsync(cancellationToken);
await _client.Delete(RecordId.From(_tableName, id), cancellationToken);
_changeFeed.PublishDelete(id);
}
public TEntity? FindById(string id)
{
return FindByIdAsync(id).GetAwaiter().GetResult();
}
public async Task<TEntity?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Document id is required.", nameof(id));
await EnsureReadyAsync(cancellationToken);
var record = await _client.Select<SampleEntityRecord<TEntity>>(RecordId.From(_tableName, id), cancellationToken);
return record?.Entity;
}
public IEnumerable<TEntity> FindAll()
{
return FindAllAsync().GetAwaiter().GetResult();
}
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
var rows = await _client.Select<SampleEntityRecord<TEntity>>(_tableName, cancellationToken);
return rows?
.Where(r => r.Entity != null)
.Select(r => r.Entity!)
.ToList()
?? [];
}
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
return FindAll().Where(predicate);
}
public void Dispose()
{
_changeFeed.Dispose();
}
private async Task UpsertAsync(TEntity entity, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(entity);
string key = _keySelector(entity);
if (string.IsNullOrWhiteSpace(key))
throw new InvalidOperationException("Entity key cannot be null or empty.");
await EnsureReadyAsync(cancellationToken);
await _client.Upsert<SampleEntityRecord<TEntity>, SampleEntityRecord<TEntity>>(
RecordId.From(_tableName, key),
new SampleEntityRecord<TEntity> { Entity = entity },
cancellationToken);
_changeFeed.PublishPut(entity, key);
}
private async Task EnsureReadyAsync(CancellationToken cancellationToken)
{
await _schemaInitializer.EnsureInitializedAsync(cancellationToken);
}
}
public sealed class SampleSurrealReadOnlyCollection<TEntity>
where TEntity : class
{
private readonly ISurrealDbClient _client;
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
private readonly string _tableName;
public SampleSurrealReadOnlyCollection(
string tableName,
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer)
{
if (string.IsNullOrWhiteSpace(tableName))
throw new ArgumentException("Table name is required.", nameof(tableName));
_tableName = tableName;
_client = (surrealEmbeddedClient ?? throw new ArgumentNullException(nameof(surrealEmbeddedClient))).Client;
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
}
public IEnumerable<TEntity> FindAll()
{
return FindAllAsync().GetAwaiter().GetResult();
}
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
{
await _schemaInitializer.EnsureInitializedAsync(cancellationToken);
var rows = await _client.Select<TEntity>(_tableName, cancellationToken);
return rows?.ToList() ?? [];
}
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
return FindAll().Where(predicate);
}
}
public sealed class SampleEntityRecord<TEntity> : Record
where TEntity : class
{
[JsonPropertyName("entity")]
public TEntity? Entity { get; set; }
}
public sealed class SampleOplogEntry : Record
{
[JsonPropertyName("collection")]
public string Collection { get; set; } = "";
[JsonPropertyName("key")]
public string Key { get; set; } = "";
[JsonPropertyName("operation")]
public int Operation { get; set; }
[JsonPropertyName("timestampNodeId")]
public string TimestampNodeId { get; set; } = "";
[JsonPropertyName("timestampPhysicalTime")]
public long TimestampPhysicalTime { get; set; }
[JsonPropertyName("timestampLogicalCounter")]
public int TimestampLogicalCounter { get; set; }
[JsonPropertyName("hash")]
public string Hash { get; set; } = "";
}

View File

@@ -3,90 +3,125 @@ using Microsoft.Extensions.Logging;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
namespace ZB.MOM.WW.CBDDC.Sample.Console;
/// <summary>
/// Document store implementation for CBDDC Sample using BLite persistence.
/// Extends BLiteDocumentStore to automatically handle Oplog creation via CDC.
/// Surreal-backed document store for the sample app.
/// </summary>
public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
{
private const string UsersCollection = "Users";
private const string TodoListsCollection = "TodoLists";
/// <summary>
/// Initializes a new instance of the <see cref="SampleDocumentStore" /> class.
/// </summary>
/// <param name="context">The sample database context.</param>
/// <param name="configProvider">The peer node configuration provider.</param>
/// <param name="vectorClockService">The vector clock service.</param>
/// <param name="logger">The optional logger instance.</param>
public SampleDocumentStore(
SampleDbContext context,
IPeerNodeConfigurationProvider configProvider,
IVectorClockService vectorClockService,
ILogger<SampleDocumentStore>? logger = null)
: base(context, configProvider, vectorClockService, new LastWriteWinsConflictResolver(), logger)
: base(
context,
context.SurrealEmbeddedClient,
context.SchemaInitializer,
configProvider,
vectorClockService,
new LastWriteWinsConflictResolver(),
null,
null,
logger)
{
// Register CDC watchers for local change detection
// InterestedCollection is automatically populated
WatchCollection(UsersCollection, context.Users, u => u.Id);
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
}
#region Helper Methods
private static JsonElement? SerializeEntity<T>(T? entity) where T : class
{
if (entity == null) return null;
return JsonSerializer.SerializeToElement(entity);
}
#endregion
#region Abstract Method Implementations
/// <inheritdoc />
protected override async Task ApplyContentToEntityAsync(
string collection, string key, JsonElement content, CancellationToken cancellationToken)
string collection,
string key,
JsonElement content,
CancellationToken cancellationToken)
{
UpsertEntity(collection, key, content);
await _context.SaveChangesAsync(cancellationToken);
await UpsertEntityAsync(collection, key, content, cancellationToken);
}
/// <inheritdoc />
protected override async Task ApplyContentToEntitiesBatchAsync(
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
CancellationToken cancellationToken)
{
foreach ((string collection, string key, var content) in documents) UpsertEntity(collection, key, content);
await _context.SaveChangesAsync(cancellationToken);
foreach ((string collection, string key, var content) in documents)
await UpsertEntityAsync(collection, key, content, cancellationToken);
}
private void UpsertEntity(string collection, string key, JsonElement content)
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
string collection,
string key,
CancellationToken cancellationToken)
{
return collection switch
{
UsersCollection => SerializeEntity(await _context.Users.FindByIdAsync(key, cancellationToken)),
TodoListsCollection => SerializeEntity(await _context.TodoLists.FindByIdAsync(key, cancellationToken)),
_ => null
};
}
protected override async Task RemoveEntityAsync(
string collection,
string key,
CancellationToken cancellationToken)
{
await DeleteEntityAsync(collection, key, cancellationToken);
}
protected override async Task RemoveEntitiesBatchAsync(
IEnumerable<(string Collection, string Key)> documents,
CancellationToken cancellationToken)
{
foreach ((string collection, string key) in documents)
await DeleteEntityAsync(collection, key, cancellationToken);
}
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
string collection,
CancellationToken cancellationToken)
{
return collection switch
{
UsersCollection => (await _context.Users.FindAllAsync(cancellationToken))
.Select(u => (u.Id, SerializeEntity(u)!.Value))
.ToList(),
TodoListsCollection => (await _context.TodoLists.FindAllAsync(cancellationToken))
.Select(t => (t.Id, SerializeEntity(t)!.Value))
.ToList(),
_ => []
};
}
private async Task UpsertEntityAsync(
string collection,
string key,
JsonElement content,
CancellationToken cancellationToken)
{
switch (collection)
{
case UsersCollection:
var user = content.Deserialize<User>()!;
var user = content.Deserialize<User>() ?? throw new InvalidOperationException("Failed to deserialize user.");
user.Id = key;
var existingUser = _context.Users.Find(u => u.Id == key).FirstOrDefault();
if (existingUser != null)
_context.Users.Update(user);
if (await _context.Users.FindByIdAsync(key, cancellationToken) == null)
await _context.Users.InsertAsync(user, cancellationToken);
else
_context.Users.Insert(user);
await _context.Users.UpdateAsync(user, cancellationToken);
break;
case TodoListsCollection:
var todoList = content.Deserialize<TodoList>()!;
todoList.Id = key;
var existingTodoList = _context.TodoLists.Find(t => t.Id == key).FirstOrDefault();
if (existingTodoList != null)
_context.TodoLists.Update(todoList);
var todo = content.Deserialize<TodoList>() ??
throw new InvalidOperationException("Failed to deserialize todo list.");
todo.Id = key;
if (await _context.TodoLists.FindByIdAsync(key, cancellationToken) == null)
await _context.TodoLists.InsertAsync(todo, cancellationToken);
else
_context.TodoLists.Insert(todoList);
await _context.TodoLists.UpdateAsync(todo, cancellationToken);
break;
default:
@@ -94,43 +129,15 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
}
}
/// <inheritdoc />
protected override Task<JsonElement?> GetEntityAsJsonAsync(
string collection, string key, CancellationToken cancellationToken)
{
return Task.FromResult(collection switch
{
UsersCollection => SerializeEntity(_context.Users.Find(u => u.Id == key).FirstOrDefault()),
TodoListsCollection => SerializeEntity(_context.TodoLists.Find(t => t.Id == key).FirstOrDefault()),
_ => null
});
}
/// <inheritdoc />
protected override async Task RemoveEntityAsync(
string collection, string key, CancellationToken cancellationToken)
{
DeleteEntity(collection, key);
await _context.SaveChangesAsync(cancellationToken);
}
/// <inheritdoc />
protected override async Task RemoveEntitiesBatchAsync(
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken)
{
foreach ((string collection, string key) in documents) DeleteEntity(collection, key);
await _context.SaveChangesAsync(cancellationToken);
}
private void DeleteEntity(string collection, string key)
private async Task DeleteEntityAsync(string collection, string key, CancellationToken cancellationToken)
{
switch (collection)
{
case UsersCollection:
_context.Users.Delete(key);
await _context.Users.DeleteAsync(key, cancellationToken);
break;
case TodoListsCollection:
_context.TodoLists.Delete(key);
await _context.TodoLists.DeleteAsync(key, cancellationToken);
break;
default:
_logger.LogWarning("Attempted to remove entity from unsupported collection: {Collection}", collection);
@@ -138,21 +145,8 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
}
}
/// <inheritdoc />
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
string collection, CancellationToken cancellationToken)
private static JsonElement? SerializeEntity<T>(T? entity) where T : class
{
return await Task.Run(() => collection switch
{
UsersCollection => _context.Users.FindAll()
.Select(u => (u.Id, SerializeEntity(u)!.Value)),
TodoListsCollection => _context.TodoLists.FindAll()
.Select(t => (t.Id, SerializeEntity(t)!.Value)),
_ => Enumerable.Empty<(string, JsonElement)>()
}, cancellationToken);
return entity == null ? null : JsonSerializer.SerializeToElement(entity);
}
#endregion
}
}

View File

@@ -2,20 +2,16 @@
<ItemGroup>
<PackageReference Include="Lifter.Core" Version="1.1.0"/>
<PackageReference Include="BLite.SourceGenerators" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj"/>
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Network\ZB.MOM.WW.CBDDC.Network.csproj"/>
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Persistence\ZB.MOM.WW.CBDDC.Persistence.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4"/>
<PackageReference Include="Serilog" Version="4.2.0"/>
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>