C# FolderQueue

Date: 2025-10-08
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Domain.Processing
{
    public class FolderQueueSettings
    {
        public string QueueFolderPath { get; set; }
        public string ProcessedFolderPath { get; set; }
        public string FailedFolderPath { get; set; }

        public int MaxRetryAttempts { get; set; } = 3;
        public int RetryDelayMs { get; set; } = 2000;

        public void Validate()
        {
            if (string.IsNullOrWhiteSpace(QueueFolderPath))
                throw new ArgumentException("QueueFolderPath must be specified.");

            Directory.CreateDirectory(QueueFolderPath);

            if (!string.IsNullOrWhiteSpace(ProcessedFolderPath))
                Directory.CreateDirectory(ProcessedFolderPath);

            if (!string.IsNullOrWhiteSpace(FailedFolderPath))
                Directory.CreateDirectory(FailedFolderPath);
        }
    }

    public interface ISerializer<T>
    {
        string Serialize(T item);
        T Deserialize(string serialized);
    }

    /// <summary>
    /// Generic queue based on folder storage.
    /// Files are created in QueueFolderPath and processed sequentially.
    /// </summary>
    public class FolderQueue<T>
    {
        private readonly FolderQueueSettings _settings;
        private readonly ISerializer<T> _serializer;
        private readonly ILogger<FolderQueue<T>> _logger;

        public FolderQueue(FolderQueueSettings settings, ISerializer<T> serializer, ILogger<FolderQueue<T>> logger = null)
        {
            _settings = settings ?? throw new ArgumentNullException(nameof(settings));
            _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
            _logger = logger;
            _settings.Validate();
        }

        public async Task AddToQueueAsync(T item)
        {
            if (item == null)
                throw new ArgumentNullException(nameof(item));

            var serialized = _serializer.Serialize(item);
            var fileName = Path.Combine(_settings.QueueFolderPath, $"{DateTime.UtcNow:yyyyMMdd_HHmmss}_{Guid.NewGuid()}.json");
            await File.WriteAllTextAsync(fileName, serialized, Encoding.UTF8);

            _logger?.LogInformation("Item added to queue: {FileName}", fileName);
        }

        public async Task ProcessQueueAsync(Func<T, Task> processor)
        {
            if (processor == null)
                throw new ArgumentNullException(nameof(processor));

            var files = Directory.GetFiles(_settings.QueueFolderPath, "*.json");
            _logger?.LogInformation("Processing {Count} items from queue", files.Length);

            foreach (var file in files)
            {
                await ProcessFileAsync(file, processor);
            }
        }

        private async Task ProcessFileAsync(string filePath, Func<T, Task> processor)
        {
            var retryCount = 0;

            while (retryCount <= _settings.MaxRetryAttempts)
            {
                try
                {
                    var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
                    var item = _serializer.Deserialize(content);

                    await processor(item);

                    await MoveToProcessedFolderAsync(filePath);
                    _logger?.LogInformation("Successfully processed item: {FileName}", Path.GetFileName(filePath));
                    return;
                }
                catch (Exception ex)
                {
                    retryCount++;
                    _logger?.LogWarning(ex, "Failed to process file {FileName} (attempt {Attempt}/{Max})", Path.GetFileName(filePath), retryCount, _settings.MaxRetryAttempts + 1);

                    if (retryCount > _settings.MaxRetryAttempts)
                    {
                        await MoveToFailedFolderAsync(filePath);
                        _logger?.LogError(ex, "File moved to failed folder after {Retries} attempts: {FileName}", retryCount, Path.GetFileName(filePath));
                        return;
                    }

                    if (_settings.RetryDelayMs > 0)
                        await Task.Delay(_settings.RetryDelayMs);
                }
            }
        }

        private async Task MoveToProcessedFolderAsync(string filePath)
        {
            if (!string.IsNullOrWhiteSpace(_settings.ProcessedFolderPath))
            {
                var fileName = Path.GetFileName(filePath);
                var dest = Path.Combine(_settings.ProcessedFolderPath, fileName);
                if (File.Exists(dest)) File.Delete(dest);
                File.Move(filePath, dest);
            }
            else
            {
                File.Delete(filePath);
            }
        }

        private async Task MoveToFailedFolderAsync(string filePath)
        {
            if (!string.IsNullOrWhiteSpace(_settings.FailedFolderPath))
            {
                var fileName = Path.GetFileName(filePath);
                var dest = Path.Combine(_settings.FailedFolderPath, fileName);
                if (File.Exists(dest)) File.Delete(dest);
                File.Move(filePath, dest);
            }
        }
    }
}

Example mail queue

using System.Net.Mail;
using Microsoft.Extensions.Logging;

namespace Domain.Mail
{
    public class MailQueueService : IMailQueueService
    {
        private readonly FolderQueue<MailMessage> _queue;
        private readonly ILogger<MailQueueService> _logger;

        public MailQueueService(MailQueueSettings settings, IMailSerializer mailSerializer, ILogger<MailQueueService> logger = null)
        {
            var folderSettings = new FolderQueueSettings
            {
                QueueFolderPath = settings.QueueFolderPath,
                FailedFolderPath = settings.FailedFolderPath,
                ProcessedFolderPath = settings.ProcessedFolderPath,
                MaxRetryAttempts = settings.MaxRetryAttempts,
                RetryDelayMs = settings.RetryDelayMs
            };

            _queue = new FolderQueue<MailMessage>(folderSettings, mailSerializer, logger);
            _logger = logger;
        }

        public Task AddToQueueAsync(MailMessage message) => _queue.AddToQueueAsync(message);

        public Task ProcessQueueAsync() =>
            _queue.ProcessQueueAsync(async mail =>
            {
                using var smtp = new SmtpClientWrapper(new MailQueueSettings()); // of gebruik _settings
                await smtp.SendMailAsync(mail);
            });
    }
}

Simple Json serializer

using System.Text.Json;

public class JsonSerializerAdapter<T> : ISerializer<T>
{
    public string Serialize(T item) => JsonSerializer.Serialize(item, new JsonSerializerOptions { WriteIndented = true });
    public T Deserialize(string serialized) => JsonSerializer.Deserialize<T>(serialized);
}
97920cookie-checkC# FolderQueue