You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD中FlatFileUnitOfWork是否合法?求C#实现示例

Is a FlatFileUnitOfWork Valid in DDD?

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.


Robust C# Implementation Example

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

Key Considerations
  • Atomicity: Using File.Replace ensures 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 FlatFileUnitOfWork as a scoped service (per request/operation) so all repositories in the same scope use the same temp file.
  • File Format: Customize the ConvertOrderToFlatLine method to match your required flat file format (fixed-width, CSV, etc.).
  • Error Handling: The Dispose method automatically rolls back if the UoW isn’t committed, protecting against forgotten commits.

内容的提问来源于stack exchange,提问作者Cromm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:50:20