using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using PowderCoating.Core.Entities; using PowderCoating.Core.Interfaces; using PowderCoating.Core.Interfaces.Repositories; using PowderCoating.Infrastructure.Data; namespace PowderCoating.Infrastructure.Repositories; /// /// Concrete implementation of that coordinates all entity repositories /// and exposes transaction management for the application layer. /// /// All entity repositories are lazily instantiated — a is only /// allocated the first time the corresponding property is accessed. This keeps the per-request /// memory footprint small when only a handful of entities are needed. /// /// /// Persistence: call or /// (identical behaviour) to flush pending changes to the database. /// /// /// Explicit transactions: use / /// / when multiple /// SaveChanges calls must succeed or fail atomically. For a simpler fire-and-forget pattern, /// prefer which wraps the operation in an /// EF Core execution-strategy-aware transaction automatically. /// /// public class UnitOfWork : IUnitOfWork { private readonly ApplicationDbContext _context; private IDbContextTransaction? _transaction; // Multi-tenancy private IRepository? _companies; private IRepository? _companyOperatingCosts; private IRepository? _companyPreferences; private IRepository? _companySmsAgreements; // AI Predictions private IRepository? _aiItemPredictions; // Powder Insights private IPowderUsageLogRepository? _powderUsageLogs; // Core repositories private ICustomerRepository? _customers; private IJobRepository? _jobs; private IRepository? _jobDailyPriorities; private IRepository? _jobItems; private IJobItemCoatRepository? _jobItemCoats; private IRepository? _jobItemPrepServices; private IRepository? _jobChangeHistories; private IRepository? _jobPrepServices; private IQuoteRepository? _quotes; private IRepository? _quotePhotos; private IRepository? _quoteItems; private IRepository? _quoteItemCoats; private IRepository? _quoteItemPrepServices; private IRepository? _quoteChangeHistories; private IRepository? _inventoryItems; private IPlainRepository? _powderCatalog; private IInventoryTransactionRepository? _inventoryTransactions; private IRepository? _equipment; private IRepository? _ovenCosts; private IRepository? _blastSetups; private IRepository? _maintenanceRecords; private IRepository? _vendors; private IJobPhotoRepository? _jobPhotos; private IRepository? _jobNotes; private IRepository? _customerNotes; private IRepository? _jobStatusHistory; private IRepository? _pricingTiers; // Lookup tables (replacing enums) private IRepository? _jobStatusLookups; private IRepository? _jobPriorityLookups; private IRepository? _quoteStatusLookups; private IRepository? _inventoryCategoryLookups; private IRepository? _appointmentStatusLookups; private IRepository? _appointmentTypeLookups; private IRepository? _prepServices; // Appointments private IRepository? _appointments; // Product Catalog private IRepository? _catalogCategories; private IRepository? _catalogItems; private IRepository? _catalogPriceCheckReports; // Notifications private INotificationLogRepository? _notificationLogs; private IRepository? _notificationTemplates; // Subscription private IRepository? _subscriptionPlanConfigs; // Job Templates private IJobTemplateRepository? _jobTemplates; private IRepository? _jobTemplateItems; private IRepository? _jobTemplateItemCoats; private IRepository? _jobTemplateItemPrepServices; // Platform content private IPlainRepository? _announcements; private IPlainRepository? _bannedIps; private IPlainRepository? _dashboardTips; private IRepository? _inAppNotifications; private IPlainRepository? _releaseNotes; // Bug Reports private IRepository? _bugReports; private IRepository? _bugReportAttachments; private IRepository? _contactSubmissions; private IRepository? _manufacturerLookupPatterns; // Gift Certificates private IRepository? _giftCertificates; private IRepository? _giftCertificateRedemptions; // Customer Intake Kiosk private IRepository? _kioskSessions; // Employee Timeclock private IRepository? _employeeClockEntries; private IRepository? _timeclockKioskDevices; // Custom Formula Templates private IRepository? _customItemTemplates; // Formula Community Library private IPlainRepository? _formulaLibrary; private IRepository? _formulaLibraryImports; private IPlainRepository? _formulaLibraryRatings; // Purchase Orders private IPurchaseOrderRepository? _purchaseOrders; private IRepository? _purchaseOrderItems; // Oven Scheduling private IRepository? _ovenBatches; private IRepository? _ovenBatchItems; // Invoices, Payments & Deposits private IInvoiceRepository? _invoices; private IRepository? _invoiceItems; private IRepository? _payments; private IRepository? _deposits; // Expense Tracking / Accounts Payable private IRepository? _accounts; private IBillRepository? _bills; private IRepository? _billLineItems; private IRepository? _billPayments; private IRepository? _expenses; // Manual Journal Entries private IRepository? _journalEntries; private IRepository? _journalEntryLines; // Vendor Credits private IRepository? _vendorCredits; private IRepository? _vendorCreditLineItems; private IRepository? _vendorCreditApplications; // Bank Reconciliation private IRepository? _bankReconciliations; // Tax Rates private IRepository? _taxRates; // Recurring Transactions private IRepository? _recurringTemplates; private IRepository? _fixedAssets; private IRepository? _fixedAssetDepreciationEntries; private IRepository? _budgets; private IRepository? _budgetLines; private IRepository? _yearEndCloses; /// /// Initialises the unit of work with the scoped . /// The context is shared across all repositories created by this instance so that /// all reads and writes within a single request participate in the same EF Core identity map /// and change-tracker, enabling zero-copy entity sharing between repositories. /// public UnitOfWork(ApplicationDbContext context) { _context = context; } // ------------------------------------------------------------------------- // Repository properties — lazy-initialised via the null-coalescing assignment // operator (??=). Each property creates one Repository instance at most // per UnitOfWork lifetime (= per HTTP request when registered as Scoped). // Repositories share the underlying DbContext so they share the change tracker. // ------------------------------------------------------------------------- /// Repository for tenant records. Soft-delete filter only (no tenant filter — SuperAdmin manages all companies). public IRepository Companies => _companies ??= new Repository(_context); /// Repository for (one-to-one with Company). public IRepository CompanyOperatingCosts => _companyOperatingCosts ??= new Repository(_context); /// Repository for (one-to-one with Company). public IRepository CompanyPreferences => _companyPreferences ??= new Repository(_context); /// Repository for audit records. Tenant-filtered; never soft-deleted — legal audit trail. public IRepository CompanySmsAgreements => _companySmsAgreements ??= new Repository(_context); // AI Predictions /// Repository for records; tenant-filtered. Shared between QuoteItem and JobItem via a single nullable FK — no duplication on quote→job conversion. public IRepository AiItemPredictions => _aiItemPredictions ??= new Repository(_context); // Powder Insights /// Repository for records capturing per-coat powder consumption; used by powder-usage analytics. public IPowderUsageLogRepository PowderUsageLogs => _powderUsageLogs ??= new PowderUsageLogRepository(_context); // Core repositories /// Repository for records (commercial and non-commercial); tenant-filtered with soft delete. public ICustomerRepository Customers => _customers ??= new CustomerRepository(_context); /// Repository for records progressing through the 16-status lifecycle; tenant-filtered with soft delete. public IJobRepository Jobs => _jobs ??= new JobRepository(_context); /// Repository for overrides that let supervisors re-order the shop floor queue. public IRepository JobDailyPriorities => _jobDailyPriorities ??= new Repository(_context); /// Repository for line-items; tenant-filtered with soft delete. public IRepository JobItems => _jobItems ??= new Repository(_context); /// Repository for powder coat passes; tenant-filtered with soft delete. public IJobItemCoatRepository JobItemCoats => _jobItemCoats ??= new JobItemCoatRepository(_context); public IRepository JobItemPrepServices => _jobItemPrepServices ??= new Repository(_context); /// Repository for audit entries; tenant-filtered with soft delete. public IRepository JobChangeHistories => _jobChangeHistories ??= new Repository(_context); /// Repository for job-level prep service assignments; tenant-filtered with soft delete. public IRepository JobPrepServices => _jobPrepServices ??= new Repository(_context); /// Repository for records with multi-item pricing; tenant-filtered with soft delete. public IQuoteRepository Quotes => _quotes ??= new QuoteRepository(_context); /// Repository for AI photo uploads; tenant-filtered with soft delete. public IRepository QuotePhotos => _quotePhotos ??= new Repository(_context); /// Repository for line-items; tenant-filtered with soft delete. public IRepository QuoteItems => _quoteItems ??= new Repository(_context); /// Repository for powder coat passes on a quote item; tenant-filtered with soft delete. public IRepository QuoteItemCoats => _quoteItemCoats ??= new Repository(_context); /// Repository for prep-service assignments on a quote item; tenant-filtered with soft delete. public IRepository QuoteItemPrepServices => _quoteItemPrepServices ??= new Repository(_context); /// Repository for audit entries; tenant-filtered with soft delete. public IRepository QuoteChangeHistories => _quoteChangeHistories ??= new Repository(_context); /// Repository for powder and material stock; tenant-filtered with soft delete. public IRepository InventoryItems => _inventoryItems ??= new Repository(_context); /// Platform-level powder catalog — no tenant filter, no soft delete. public IPlainRepository PowderCatalog => _powderCatalog ??= new PlainRepository(_context); /// Repository for stock movements; tenant-filtered with soft delete. public IInventoryTransactionRepository InventoryTransactions => _inventoryTransactions ??= new InventoryTransactionRepository(_context); /// Repository for records (ovens, sandblasters, booths); tenant-filtered with soft delete. public IRepository Equipment => _equipment ??= new Repository(_context); /// Repository for named oven configurations used by the Oven Scheduler; tenant-filtered with soft delete. public IRepository OvenCosts => _ovenCosts ??= new Repository(_context); /// Repository for named blast setups; tenant-filtered with soft delete. public IRepository BlastSetups => _blastSetups ??= new Repository(_context); /// Repository for equipment maintenance records; tenant-filtered with soft delete. public IRepository MaintenanceRecords => _maintenanceRecords ??= new Repository(_context); /// Repository for supplier records; tenant-filtered with soft delete. public IRepository Vendors => _vendors ??= new Repository(_context); /// Repository for attachments; tenant-filtered with soft delete. public IJobPhotoRepository JobPhotos => _jobPhotos ??= new JobPhotoRepository(_context); /// Repository for free-text staff notes on jobs; tenant-filtered with soft delete. public IRepository JobNotes => _jobNotes ??= new Repository(_context); /// Repository for free-text staff notes on customer records; tenant-filtered with soft delete. public IRepository CustomerNotes => _customerNotes ??= new Repository(_context); /// Repository for status-transition audit records; tenant-filtered with soft delete. public IRepository JobStatusHistory => _jobStatusHistory ??= new Repository(_context); /// Repository for customer discount tiers; tenant-filtered with soft delete. public IRepository PricingTiers => _pricingTiers ??= new Repository(_context); // Lookup tables (replacing enums) // These are stored in the database instead of C# enums so that tenants can // customise display names, colours, and display order without a code deployment. /// Repository for DB-backed job status definitions. public IRepository JobStatusLookups => _jobStatusLookups ??= new Repository(_context); /// Repository for DB-backed job priority definitions. public IRepository JobPriorityLookups => _jobPriorityLookups ??= new Repository(_context); /// Repository for DB-backed quote status definitions. public IRepository QuoteStatusLookups => _quoteStatusLookups ??= new Repository(_context); /// Repository for user-defined inventory categories. public IRepository InventoryCategoryLookups => _inventoryCategoryLookups ??= new Repository(_context); /// Repository for appointment status definitions. public IRepository AppointmentStatusLookups => _appointmentStatusLookups ??= new Repository(_context); /// Repository for appointment type definitions. public IRepository AppointmentTypeLookups => _appointmentTypeLookups ??= new Repository(_context); /// Repository for prep-service catalog entries shared across quotes and jobs. public IRepository PrepServices => _prepServices ??= new Repository(_context); /// Repository for quality-failure and remediation records; tenant-filtered with soft delete. private IRepository? _reworkRecords; public IRepository ReworkRecords => _reworkRecords ??= new Repository(_context); /// Repository for customer refund records; tenant-filtered with soft delete. private IRepository? _refunds; public IRepository Refunds => _refunds ??= new Repository(_context); /// Repository for records issued to customers; unique memo number per company. private IRepository? _creditMemos; public IRepository CreditMemos => _creditMemos ??= new Repository(_context); /// Repository for records linking credit memos to specific invoices. private IRepository? _creditMemoApplications; public IRepository CreditMemoApplications => _creditMemoApplications ??= new Repository(_context); /// Repository for clock-in/clock-out worker time records; used for labour-cost calculations. private IRepository? _jobTimeEntries; public IRepository JobTimeEntries => _jobTimeEntries ??= new Repository(_context); // Appointments /// Repository for customer appointment records; tenant-filtered with soft delete. public IRepository Appointments => _appointments ??= new Repository(_context); // Product Catalog /// Repository for hierarchical service catalog categories; tenant-filtered with soft delete. public IRepository CatalogCategories => _catalogCategories ??= new Repository(_context); /// Repository for pre-priced service catalog items; tenant-filtered with soft delete. public IRepository CatalogItems => _catalogItems ??= new Repository(_context); /// Repository for AI price-check results archived per company. public IRepository CatalogPriceCheckReports => _catalogPriceCheckReports ??= new Repository(_context); // Notifications /// Repository for outbound notification audit records; provides IgnoreQueryFilters lookups by InvoiceId, QuoteId, and JobId for notification history panels. public INotificationLogRepository NotificationLogs => _notificationLogs ??= new NotificationLogRepository(_context); /// Repository for per-company channel template overrides; unique on (CompanyId, Type, Channel). public IRepository NotificationTemplates => _notificationTemplates ??= new Repository(_context); // Subscription /// Repository for Stripe plan definitions; global (no tenant filter). public IRepository SubscriptionPlanConfigs => _subscriptionPlanConfigs ??= new Repository(_context); // Platform content /// Repository for platform-wide announcements; no tenant filter, no soft delete. public IPlainRepository Announcements => _announcements ??= new PlainRepository(_context); /// Repository for IP ban records; no tenant filter, no soft delete. public IPlainRepository BannedIps => _bannedIps ??= new PlainRepository(_context); /// Repository for rotating tip-of-the-day entries; no tenant filter, no soft delete. public IPlainRepository DashboardTips => _dashboardTips ??= new PlainRepository(_context); /// Repository for bell-notification records; tenant-filtered with soft delete. public IRepository InAppNotifications => _inAppNotifications ??= new Repository(_context); /// Repository for platform changelog entries; no tenant filter, no soft delete. public IPlainRepository ReleaseNotes => _releaseNotes ??= new PlainRepository(_context); // Bug Reports /// Repository for user-submitted bug reports; tenant-filtered with soft delete. public IRepository BugReports => _bugReports ??= new Repository(_context); public IRepository BugReportAttachments => _bugReportAttachments ??= new Repository(_context); // Contact Us /// Repository for contact form submissions; platform admins see all, company users see their own. public IRepository ContactSubmissions => _contactSubmissions ??= new Repository(_context); /// Repository for global AI URL patterns; soft-delete only (CompanyId = 0 by convention). public IRepository ManufacturerLookupPatterns => _manufacturerLookupPatterns ??= new Repository(_context); // Gift Certificates /// Repository for records; certificate code unique per company. public IRepository GiftCertificates => _giftCertificates ??= new Repository(_context); /// Repository for usage events against a gift certificate. public IRepository GiftCertificateRedemptions => _giftCertificateRedemptions ??= new Repository(_context); /// Repository for customer self-service intake sessions; tenant-filtered with soft delete. public IRepository KioskSessions => _kioskSessions ??= new Repository(_context); /// Repository for facility-level clock-in/clock-out records; tenant-filtered with soft delete. Multiple entries per day are fully supported. public IRepository EmployeeClockEntries => _employeeClockEntries ??= new Repository(_context); /// Repository for activated tablet records; one row per device. Delete a row to revoke that device's access. public IRepository TimeclockKioskDevices => _timeclockKioskDevices ??= new Repository(_context); /// Repository for per-company reusable NCalc pricing formula templates; tenant-filtered with soft delete. public IRepository CustomItemTemplates => _customItemTemplates ??= new Repository(_context); /// Repository for community library entries; platform-level, no tenant filter. public IPlainRepository FormulaLibrary => _formulaLibrary ??= new PlainRepository(_context); /// Repository for per-company import records; tenant-filtered with soft delete. public IRepository FormulaLibraryImports => _formulaLibraryImports ??= new Repository(_context); /// Repository for per-company thumbs votes; platform-level, no tenant filter. public IPlainRepository FormulaLibraryRatings => _formulaLibraryRatings ??= new PlainRepository(_context); // Job Templates /// Repository for reusable job blueprints; tenant-filtered with soft delete. public IJobTemplateRepository JobTemplates => _jobTemplates ??= new JobTemplateRepository(_context); /// Repository for item definitions within a job template. public IRepository JobTemplateItems => _jobTemplateItems ??= new Repository(_context); /// Repository for coat definitions within a job template item. public IRepository JobTemplateItemCoats => _jobTemplateItemCoats ??= new Repository(_context); /// Repository for prep-service definitions within a job template item. public IRepository JobTemplateItemPrepServices => _jobTemplateItemPrepServices ??= new Repository(_context); // Purchase Orders /// Repository for vendor purchase orders; tenant-filtered with soft delete. public IPurchaseOrderRepository PurchaseOrders => _purchaseOrders ??= new PurchaseOrderRepository(_context); /// Repository for line-items on a purchase order; cascade-deleted with the PO. public IRepository PurchaseOrderItems => _purchaseOrderItems ??= new Repository(_context); // Oven Scheduling /// Repository for scheduled oven cure batches; tenant-filtered with soft delete. public IRepository OvenBatches => _ovenBatches ??= new Repository(_context); /// Repository for job/item assignments within an oven batch. public IRepository OvenBatchItems => _ovenBatchItems ??= new Repository(_context); // Invoices, Payments & Deposits /// Repository for customer invoices (1:1 with Job); tenant-filtered with soft delete. public IInvoiceRepository Invoices => _invoices ??= new InvoiceRepository(_context); /// Repository for line-items on an invoice; tenant-filtered with soft delete. public IRepository InvoiceItems => _invoiceItems ??= new Repository(_context); /// Repository for customer payment records against invoices; tenant-filtered with soft delete. public IRepository Payments => _payments ??= new Repository(_context); /// /// Repository for pre-invoice deposit records. /// Unapplied deposits are auto-swept into records when an invoice is created. /// public IRepository Deposits => _deposits ??= new Repository(_context); // Expense Tracking / Accounts Payable /// Repository for chart-of-accounts entries; supports self-referencing parent/child hierarchy. public IRepository Accounts => _accounts ??= new Repository(_context); /// Repository for vendor bills (accounts payable); tenant-filtered with soft delete. public IBillRepository Bills => _bills ??= new BillRepository(_context); /// Repository for expense line-items on a vendor bill; each assigned to a chart-of-accounts entry. public IRepository BillLineItems => _billLineItems ??= new Repository(_context); /// Repository for cash disbursement records against a vendor bill. public IRepository BillPayments => _billPayments ??= new Repository(_context); /// Repository for ad-hoc non-bill expense records; tenant-filtered with soft delete. public IRepository Expenses => _expenses ??= new Repository(_context); // Manual Journal Entries /// Repository for double-entry manual journal entries; tenant-filtered with soft delete. public IRepository JournalEntries => _journalEntries ??= new Repository(_context); /// Repository for individual debit/credit lines within a journal entry. public IRepository JournalEntryLines => _journalEntryLines ??= new Repository(_context); // Vendor Credits /// Repository for credit notes received from vendors; tenant-filtered with soft delete. public IRepository VendorCredits => _vendorCredits ??= new Repository(_context); /// Repository for expense-reversal lines on a vendor credit. public IRepository VendorCreditLineItems => _vendorCreditLineItems ??= new Repository(_context); /// Repository for records linking a vendor credit to a specific bill. public IRepository VendorCreditApplications => _vendorCreditApplications ??= new Repository(_context); // Bank Reconciliation /// Repository for sessions reconciling a bank account against a statement. public IRepository BankReconciliations => _bankReconciliations ??= new Repository(_context); // Tax Rates /// Repository for named tax rates used to pre-fill invoice tax percent by jurisdiction. public IRepository TaxRates => _taxRates ??= new Repository(_context); // Recurring Transactions /// Repository for — saved recipes that auto-generate bills or expenses on a schedule. public IRepository RecurringTemplates => _recurringTemplates ??= new Repository(_context); public IRepository FixedAssets => _fixedAssets ??= new Repository(_context); public IRepository FixedAssetDepreciationEntries => _fixedAssetDepreciationEntries ??= new Repository(_context); public IRepository Budgets => _budgets ??= new Repository(_context); public IRepository BudgetLines => _budgetLines ??= new Repository(_context); public IRepository YearEndCloses => _yearEndCloses ??= new Repository(_context); /// /// Flushes all pending changes in the EF Core change tracker to the database. /// Returns the number of state entries written. /// /// Internally delegates to which /// automatically stamps CreatedAt, UpdatedAt, CompanyId, and audit /// fields on every modified entity before the SQL is sent. /// /// public async Task SaveChangesAsync() { return await _context.SaveChangesAsync(); } /// /// Alias for ; provided so controllers can use the more /// expressive await _unitOfWork.CompleteAsync() idiom that signals intent /// ("I am done with this unit of work") rather than the more technical "save changes". /// Both methods are functionally identical. /// public async Task CompleteAsync() { return await SaveChangesAsync(); } /// /// Executes inside a database transaction that is automatically /// committed if the operation succeeds or rolled back if it throws. /// /// Uses the EF Core IExecutionStrategy so that the transaction is automatically /// retried on transient SQL errors (e.g., deadlocks) when the database provider supports it. /// This is the preferred approach over the manual / /// pair for most use cases. /// /// /// An async delegate whose work must be atomic. Do NOT call SaveChangesAsync inside — it is called automatically before commit. public async Task ExecuteInTransactionAsync(Func operation) { var strategy = _context.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync( null, async (dbCtx, state, ct) => { await using var tx = await _context.Database.BeginTransactionAsync(ct); try { await operation(); await _context.SaveChangesAsync(ct); await tx.CommitAsync(ct); return true; } catch { await tx.RollbackAsync(ct); throw; } }, null); } /// /// Executes inside an execution-strategy-aware database transaction /// and returns the result produced by the operation. /// /// Identical to but for operations that /// produce a value (e.g., returning a newly created entity or a generated document number). /// SaveChangesAsync is called automatically before the transaction is committed. /// /// /// The type of value returned by the operation. /// An async delegate that returns a value and whose work must be atomic. /// The value returned by . public async Task ExecuteInTransactionAsync(Func> operation) { var strategy = _context.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync( null, async (dbCtx, state, ct) => { await using var tx = await _context.Database.BeginTransactionAsync(ct); try { var result = await operation(); await _context.SaveChangesAsync(ct); await tx.CommitAsync(ct); return result; } catch { await tx.RollbackAsync(ct); throw; } }, null); } /// /// Detaches all tracked entities from the EF Core change tracker. /// /// Use this in long-running processes (e.g., bulk import, seed data) to prevent the /// change tracker from accumulating thousands of entity snapshots, which would cause /// quadratic memory and CPU growth as EF Core compares snapshots on each SaveChanges. /// Not typically needed in per-request controllers because the DbContext is scoped. /// /// public void ClearChangeTracker() { _context.ChangeTracker.Clear(); } /// /// Starts an explicit database transaction and stores it in _transaction. /// Use this when you need to interleave multiple calls /// within one atomic operation, or when you need finer control than /// provides. /// Always pair with a matching or /// in a try/finally block. /// public async Task BeginTransactionAsync() { _transaction = await _context.Database.BeginTransactionAsync(); } /// /// Saves all pending changes and commits the active transaction. /// If the save or commit fails the transaction is automatically rolled back and /// disposed, and the exception is re-thrown so callers can handle it. /// The _transaction field is nulled after disposal so the instance is safe to reuse. /// public async Task CommitTransactionAsync() { try { await SaveChangesAsync(); if (_transaction != null) { await _transaction.CommitAsync(); } } catch { await RollbackTransactionAsync(); throw; } finally { if (_transaction != null) { await _transaction.DisposeAsync(); _transaction = null; } } } /// /// Rolls back the active transaction (if one exists), discarding all changes made since /// was called, then disposes and nulls the transaction. /// Safe to call even if no transaction is active — the null check prevents an exception. /// public async Task RollbackTransactionAsync() { if (_transaction != null) { await _transaction.RollbackAsync(); await _transaction.DisposeAsync(); _transaction = null; } } /// /// Disposes the active transaction (if any) and the underlying . /// Called automatically by the ASP.NET Core DI container at the end of each HTTP request /// because UnitOfWork is registered as a scoped service. /// Explicit disposal is only needed in non-DI scenarios (e.g., unit tests). /// public void Dispose() { _transaction?.Dispose(); _context.Dispose(); } }