using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core.CDC;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Compression;
using ZB.MOM.WW.CBDD.Core.Metadata;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Transactions;
namespace ZB.MOM.WW.CBDD.Core;
internal interface ICompactionAwareCollection
{
///
/// Refreshes index bindings after compaction.
///
void RefreshIndexBindingsAfterCompaction();
}
///
/// Base class for database contexts.
/// Inherit and add DocumentCollection{T} properties for your entities.
/// Use partial class for Source Generator integration.
///
public abstract class DocumentDbContext : IDisposable, ITransactionHolder
{
internal readonly ChangeStreamDispatcher _cdc;
private readonly List _compactionAwareCollections = new();
private readonly IReadOnlyDictionary _model;
private readonly List _registeredMappers = new();
private readonly IStorageEngine _storage;
private readonly SemaphoreSlim _transactionLock = new(1, 1);
protected bool _disposed;
///
/// Creates a new database context with default configuration
///
/// The database file path.
protected DocumentDbContext(string databasePath)
: this(databasePath, PageFileConfig.Default, CompressionOptions.Default)
{
}
///
/// Creates a new database context with default storage configuration and custom compression settings.
///
/// The database file path.
/// Compression behavior options.
protected DocumentDbContext(string databasePath, CompressionOptions compressionOptions)
: this(databasePath, PageFileConfig.Default, compressionOptions)
{
}
///
/// Creates a new database context with custom configuration
///
/// The database file path.
/// The page file configuration.
protected DocumentDbContext(string databasePath, PageFileConfig config)
: this(databasePath, config, CompressionOptions.Default)
{
}
///
/// Creates a new database context with custom storage and compression configuration.
///
/// The database file path.
/// The page file configuration.
/// Compression behavior options.
/// Maintenance scheduling options.
protected DocumentDbContext(
string databasePath,
PageFileConfig config,
CompressionOptions? compressionOptions,
MaintenanceOptions? maintenanceOptions = null)
{
if (string.IsNullOrWhiteSpace(databasePath))
throw new ArgumentNullException(nameof(databasePath));
_storage = new StorageEngine(databasePath, config, compressionOptions, maintenanceOptions);
_cdc = new ChangeStreamDispatcher();
_storage.RegisterCdc(_cdc);
// Initialize model before collections
var modelBuilder = new ModelBuilder();
OnModelCreating(modelBuilder);
_model = modelBuilder.GetEntityBuilders();
InitializeCollections();
}
///
/// Gets the current active transaction, if any.
///
public ITransaction? CurrentTransaction
{
get
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return field != null && field.State == TransactionState.Active ? field : null;
}
private set;
}
///
/// Gets the concrete storage engine for advanced scenarios in derived contexts.
///
protected StorageEngine Engine => (StorageEngine)_storage;
///
/// Gets compression options bound to this context's storage engine.
///
protected CompressionOptions CompressionOptions => _storage.CompressionOptions;
///
/// Gets the compression service for codec operations.
///
protected CompressionService CompressionService => _storage.CompressionService;
///
/// Gets compression telemetry counters.
///
protected CompressionTelemetry CompressionTelemetry => _storage.CompressionTelemetry;
///
/// Releases resources used by the context.
///
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_storage?.Dispose();
_cdc?.Dispose();
_transactionLock?.Dispose();
GC.SuppressFinalize(this);
}
///
/// Gets the current active transaction or starts a new one.
///
/// The active transaction.
public ITransaction GetCurrentTransactionOrStart()
{
return BeginTransaction();
}
///
/// Gets the current active transaction or starts a new one asynchronously.
///
/// The active transaction.
public async Task GetCurrentTransactionOrStartAsync()
{
return await BeginTransactionAsync();
}
///
/// Initializes document collections for the context.
///
protected virtual void InitializeCollections()
{
// Derived classes can override to initialize collections
}
///
/// Override to configure the model using Fluent API.
///
/// The model builder instance.
protected virtual void OnModelCreating(ModelBuilder modelBuilder)
{
}
///
/// Helper to create a DocumentCollection instance with custom TId.
/// Used by derived classes in InitializeCollections for typed primary keys.
///
/// The document identifier type.
/// The document type.
/// The mapper used for document serialization and key access.
/// The created document collection.
protected DocumentCollection CreateCollection(IDocumentMapper mapper)
where T : class
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
string? customName = null;
EntityTypeBuilder? builder = null;
if (_model.TryGetValue(typeof(T), out object? builderObj))
{
builder = builderObj as EntityTypeBuilder;
customName = builder?.CollectionName;
}
_registeredMappers.Add(mapper);
var collection = new DocumentCollection(_storage, this, mapper, customName);
if (collection is ICompactionAwareCollection compactionAwareCollection)
_compactionAwareCollections.Add(compactionAwareCollection);
// Apply configurations from ModelBuilder
if (builder != null)
foreach (var indexBuilder in builder.Indexes)
collection.ApplyIndexBuilder(indexBuilder);
_storage.RegisterMappers(_registeredMappers);
return collection;
}
///
/// Gets the document collection for the specified entity type using an ObjectId as the key.
///
/// The type of entity to retrieve the document collection for. Must be a reference type.
/// A DocumentCollection<ObjectId, T> instance for the specified entity type.
public DocumentCollection Set() where T : class
{
return Set();
}
///
/// Gets a collection for managing documents of type T, identified by keys of type TId.
/// Override is generated automatically by the Source Generator for partial DbContext classes.
///
/// The type of the unique identifier for documents in the collection.
/// The type of the document to be managed. Must be a reference type.
/// A DocumentCollection<TId, T> instance for performing operations on documents of type T.
public virtual DocumentCollection Set() where T : class
{
throw new InvalidOperationException(
$"No collection registered for entity type '{typeof(T).Name}' with key type '{typeof(TId).Name}'.");
}
///
/// Begins a transaction or returns the current active transaction.
///
/// The active transaction.
public ITransaction BeginTransaction()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
_transactionLock.Wait();
try
{
if (CurrentTransaction != null)
return CurrentTransaction; // Return existing active transaction
CurrentTransaction = _storage.BeginTransaction();
return CurrentTransaction;
}
finally
{
_transactionLock.Release();
}
}
///
/// Begins a transaction asynchronously or returns the current active transaction.
///
/// The cancellation token.
/// The active transaction.
public async Task BeginTransactionAsync(CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
var lockAcquired = false;
try
{
await _transactionLock.WaitAsync(ct);
lockAcquired = true;
if (CurrentTransaction != null)
return CurrentTransaction; // Return existing active transaction
CurrentTransaction = await _storage.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct);
return CurrentTransaction;
}
finally
{
if (lockAcquired)
_transactionLock.Release();
}
}
///
/// Commits the current transaction if one is active.
///
public void SaveChanges()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
if (CurrentTransaction != null)
try
{
CurrentTransaction.Commit();
}
finally
{
CurrentTransaction = null;
}
}
///
/// Commits the current transaction asynchronously if one is active.
///
/// The cancellation token.
public async Task SaveChangesAsync(CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
if (CurrentTransaction != null)
try
{
await CurrentTransaction.CommitAsync(ct);
}
finally
{
CurrentTransaction = null;
}
}
///
/// Executes a checkpoint using the requested mode.
///
/// Checkpoint mode to execute.
/// The checkpoint execution result.
public CheckpointResult Checkpoint(CheckpointMode mode = CheckpointMode.Truncate)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.Checkpoint(mode);
}
///
/// Executes a checkpoint asynchronously using the requested mode.
///
/// Checkpoint mode to execute.
/// The cancellation token.
/// The checkpoint execution result.
public Task CheckpointAsync(CheckpointMode mode = CheckpointMode.Truncate,
CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.CheckpointAsync(mode, ct);
}
///
/// Returns a point-in-time snapshot of compression telemetry counters.
///
public CompressionStats GetCompressionStats()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetCompressionStats();
}
///
/// Runs offline compaction by default. Set options to online mode for a bounded online pass.
///
/// Compaction execution options.
public CompactionStats Compact(CompactionOptions? options = null)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
var stats = Engine.Compact(options);
RefreshCollectionBindingsAfterCompaction();
return stats;
}
///
/// Runs offline compaction by default. Set options to online mode for a bounded online pass.
///
/// Compaction execution options.
/// Cancellation token for the asynchronous operation.
public Task CompactAsync(CompactionOptions? options = null, CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return CompactAsyncCore(options, ct);
}
///
/// Alias for .
///
/// Compaction execution options.
public CompactionStats Vacuum(CompactionOptions? options = null)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
var stats = Engine.Vacuum(options);
RefreshCollectionBindingsAfterCompaction();
return stats;
}
///
/// Async alias for .
///
/// Compaction execution options.
/// Cancellation token for the asynchronous operation.
public Task VacuumAsync(CompactionOptions? options = null, CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return VacuumAsyncCore(options, ct);
}
private async Task CompactAsyncCore(CompactionOptions? options, CancellationToken ct)
{
var stats = await Engine.CompactAsync(options, ct);
RefreshCollectionBindingsAfterCompaction();
return stats;
}
private async Task VacuumAsyncCore(CompactionOptions? options, CancellationToken ct)
{
var stats = await Engine.VacuumAsync(options, ct);
RefreshCollectionBindingsAfterCompaction();
return stats;
}
private void RefreshCollectionBindingsAfterCompaction()
{
foreach (var collection in _compactionAwareCollections) collection.RefreshIndexBindingsAfterCompaction();
}
///
/// Gets page usage grouped by page type.
///
public IReadOnlyList GetPageUsageByPageType()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetPageUsageByPageType();
}
///
/// Gets per-collection page usage diagnostics.
///
public IReadOnlyList GetPageUsageByCollection()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetPageUsageByCollection();
}
///
/// Gets per-collection compression ratio diagnostics.
///
public IReadOnlyList GetCompressionRatioByCollection()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetCompressionRatioByCollection();
}
///
/// Gets free-list summary diagnostics.
///
public FreeListSummary GetFreeListSummary()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetFreeListSummary();
}
///
/// Gets page-level fragmentation diagnostics.
///
public FragmentationMapReport GetFragmentationMap()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.GetFragmentationMap();
}
///
/// Runs compression migration as dry-run estimation by default.
///
/// Compression migration options.
public CompressionMigrationResult MigrateCompression(CompressionMigrationOptions? options = null)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.MigrateCompression(options);
}
///
/// Runs compression migration asynchronously as dry-run estimation by default.
///
/// Compression migration options.
/// Cancellation token for the asynchronous operation.
public Task MigrateCompressionAsync(CompressionMigrationOptions? options = null,
CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DocumentDbContext));
return Engine.MigrateCompressionAsync(options, ct);
}
}