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); } }