DDD中FlatFileUnitOfWork是否合法?求C#实现示例
Great question! Absolutely, a FlatFileUnitOfWork is not just valid in DDD—it’s a smart way to apply transactional consistency to file-based operations, mirroring how you’d use UoW for database interactions.
The core idea of the Unit of Work pattern is to manage a transactional boundary: a set of operations that must either all succeed or all fail (atomicity). Whether you’re writing to a database, a message queue, or a flat file, if you need to guarantee that partial updates don’t persist, UoW is the right tool for the job. In DDD, this aligns perfectly with keeping your domain logic decoupled from infrastructure concerns—you can treat the flat file as just another persistence mechanism, wrapped behind familiar abstractions like UoW and repositories.
Let’s build a practical implementation that ensures atomicity for flat file writes. The key trick here is using a temporary file to stage all changes; we only replace the target file when the unit of work is committed. If anything fails, we discard the temp file to avoid partial writes.
1. Define the Core Abstractions
First, start with a generic IUnitOfWork interface (you might already have this for your database UoW):
public interface IUnitOfWork : IDisposable { Task CommitAsync(CancellationToken cancellationToken = default); void Rollback(); }
Next, define a repository interface for your aggregate (we’ll use an Order aggregate as an example):
public interface IOrderRepository { void Add(Order order); // Add other methods as needed (e.g., Update, Delete) }
2. Implement the FlatFileUnitOfWork
This implementation will handle staging changes to a temp file and committing them atomically:
public class FlatFileUnitOfWork : IUnitOfWork { private readonly string _targetFilePath; private readonly string _tempFilePath; private readonly StreamWriter _tempFileWriter; private bool _isDisposed; private bool _isCommitted; public FlatFileUnitOfWork(string targetFilePath) { _targetFilePath = targetFilePath; // Create a unique temp file in the same directory as the target _tempFilePath = Path.Combine(Path.GetDirectoryName(targetFilePath) ?? "", $"{Path.GetFileNameWithoutExtension(targetFilePath)}_{Guid.NewGuid()}{Path.GetExtension(targetFilePath)}"); // Open the temp file for writing (append mode, in case we add multiple aggregates) _tempFileWriter = new StreamWriter(_tempFilePath, append: true, encoding: Encoding.UTF8) { AutoFlush = true }; } // Expose the temp writer to repositories so they can write to it internal StreamWriter TempFileWriter => _tempFileWriter; public async Task CommitAsync(CancellationToken cancellationToken = default) { if (_isCommitted) throw new InvalidOperationException("Unit of work has already been committed."); // Flush and close the temp file first await _tempFileWriter.FlushAsync(cancellationToken); _tempFileWriter.Close(); // Replace the target file atomically (works across Windows/macOS/Linux) if (File.Exists(_targetFilePath)) { // Optional: Create a backup of the existing file var backupPath = $"{_targetFilePath}.bak"; File.Replace(_tempFilePath, _targetFilePath, backupPath); } else { File.Move(_tempFilePath, _targetFilePath); } _isCommitted = true; } public void Rollback() { if (_isCommitted) return; // Close the writer and delete the temp file _tempFileWriter.Close(); if (File.Exists(_tempFilePath)) { File.Delete(_tempFilePath); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_isDisposed) return; if (disposing) { // Auto-rollback if the UoW wasn't committed if (!_isCommitted) { Rollback(); } _tempFileWriter.Dispose(); } _isDisposed = true; } ~FlatFileUnitOfWork() { Dispose(false); } }
3. Implement the FlatFile Repository
Now create a repository that uses the FlatFileUnitOfWork to write aggregates to the temp file:
public class FlatFileOrderRepository : IOrderRepository { private readonly FlatFileUnitOfWork _unitOfWork; // Inject the FlatFileUnitOfWork (ensure this is scoped to the same UoW instance) public FlatFileOrderRepository(FlatFileUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public void Add(Order order) { // Convert the Order aggregate to your flat file format (e.g., CSV, fixed-width) var flatFileLine = ConvertOrderToFlatLine(order); _unitOfWork.TempFileWriter.WriteLine(flatFileLine); } // Helper method to map aggregate to flat file line (customize this for your format) private string ConvertOrderToFlatLine(Order order) { // Example: CSV format with order ID, customer ID, total, and line items var lineItems = string.Join("|", order.LineItems.Select(li => $"{li.ProductId},{li.Quantity},{li.UnitPrice}")); return $"{order.Id},{order.CustomerId},{order.TotalAmount:C},{lineItems}"; } }
4. Usage in an Application Service
Here’s how you’d use this in a DDD application service to ensure atomic writes:
public class OrderProcessingService { private readonly IOrderRepository _orderRepository; private readonly IUnitOfWork _unitOfWork; public OrderProcessingService(IOrderRepository orderRepository, IUnitOfWork unitOfWork) { _orderRepository = orderRepository; _unitOfWork = unitOfWork; } public async Task ProcessAndExportOrdersAsync(IEnumerable<Order> orders, CancellationToken cancellationToken = default) { try { // Add all orders to the repository (they get written to the temp file) foreach (var order in orders) { // Apply domain logic first (e.g., mark order as exported) order.MarkAsExported(); _orderRepository.Add(order); } // Commit the unit of work to replace the target file await _unitOfWork.CommitAsync(cancellationToken); } catch (Exception ex) { // Rollback if anything fails _unitOfWork.Rollback(); // Log and rethrow or handle as needed throw new ApplicationException("Failed to export orders to flat file", ex); } } }
- Atomicity: Using
File.Replaceensures the target file is only updated if the entire temp file is ready, preventing partial writes if the process crashes mid-operation. - Dependency Injection: Register
FlatFileUnitOfWorkas a scoped service (per request/operation) so all repositories in the same scope use the same temp file. - File Format: Customize the
ConvertOrderToFlatLinemethod to match your required flat file format (fixed-width, CSV, etc.). - Error Handling: The
Disposemethod automatically rolls back if the UoW isn’t committed, protecting against forgotten commits.
内容的提问来源于stack exchange,提问作者Cromm

