Отслеживание файлов FileSystemWatcher и отправка их по API

Я не совсем знаю как правильно описать бизнес-задачу, но она выглядит примерно следующим образом: Добавляется в папку n файлов, мне их надо пачкой (т.е все 3 сразу) отправить по API. Не последовательно 3, а сразу 3 Если я реагирую на FileSystemWatcher.Created и внутри обработчика событий буду отправлять файлы - это будет некорректно. Есть необходимость отправлять документы "пачкой" (т.е в обработчике события метод АPI SendFiles(IEnumerable<string> files) который отправляет файлы контрагенту не вызвать, ибо иногда бывает необходимо отправлять не 1 файл, а 2 сразу (на сайте будет отображаться как пачка документов)). Но FileSystemWatcher реагирует на изменение файловой системы для каждого файла.. поэтому я даже ума не приложу как мне реализовать такой функционал.

Если не особо понятно что я имею в виду, то вот по пунктам:

  1. FileSystemWatcher может реагировать на n документов, но только последовательно. Т.е если будет 3 документа добавлено в папку - то будет 3 вызова события и три раза вызван метод API
  2. Отправлять документы в обработчике события не могу, потому что может быть потребность отправлять их не по одному, а сразу пачкой.

У меня сейчас примерно такие мысли по реализации:

  1. Отслеживаем добавление файла
  2. Добавляем в коллекцию
  3. Отправляем. Правда вот не знаю где мне вызывать метод отправки.
  4. Чистим коллекцию и ждем изменений снова.
static List<string> Files = new List<string>();
static void Main(string[] args)
{
    var watcher = new FileSystemWatcher(path2);
    watcher.Created += OnChanged;
    watcher.EnableRaisingEvents = true;

    Console.ReadLine();
}

private static void OnChanged(object sender, FileSystemEventArgs e)
{
    Files.Add(e.FullName);
}

private static void Send(IEnumerable<string> files)
{
    //Тут отправка по API пакета документов. Не через цикл.
}

Ответы (2 шт):

Автор решения: aepot

Вас спасет шаблон проектирования Producer/Consumer.

Самая простая реализация Producer/Consumer - это коллекция BlockingCollection, она умеет блокировать поток, пока не в нее что-то не добавишь. Ожидание новых объектов в этой коллекции синхронное, поэтму обработку надо запускать в отдельном потоке.

Пример:

static void ConsumerLoop(BlockingCollection<string> collection, int delay)
{
    while (true)
    {
        List<string> items = new List<string>();
        try
        {
            do
            {
                do
                {
                    items.Add(collection.Take());
                }
                while (collection.Count > 0);
                Thread.Sleep(delay);
            }
            while (collection.Count > 0);
        }
        catch (InvalidOperationException)
        { 
            break; 
        }
        finally
        {
            if (items.Count > 0)
                Console.WriteLine("output: " + string.Join(" ", items));
        }
    }
}

В блоке finally как раз можно вызывать отправку документов.

Пример использования:

static async Task Main(string[] args)
{
    BlockingCollection<string> collection = new BlockingCollection<string>();
    Task task = Task.Run(() => ConsumerLoop(collection, 1000));
    while (true)
    {
        string input = Console.ReadLine();
        if (input.Length == 0)
            break;
        collection.Add(input);
    }
    collection.CompleteAdding();
    await task;
}

collection.Add(input); - стоит как раз вызывать когда случилось событие добавления элемента. 1000 - сколько ждать в миллисекундах после добавления последнего элемента. То есть если элементы добавляются с периодом меньше секунды - они группируются вместе. Если прошло больше секунды, происходит обработка группы и переход к ожиданию элементов следующей группы.

Пример вывода в консоль

1
2
3
4
output: 1 2 3 4
5
5
5
5
5
5
5
output: 5 5 5 5 5 5 5
1
1
output: 1 1

Если хочется асинхронности без вызова потока, посмотрите на модные System.Threading.Channels вместо BlockingCollection, работать с ними немного сложнее, но в плане асинхронной работы с данными они лучше.


Кстати, по поводу вашего прошлого вопроса по отслеживанию дубликатных событий. В этой реализации оно элементарно решается заменой List на HashSet. То есть дубликаты будут исключаться на этапе группировки автоматически.

→ Ссылка
Автор решения: Alexander Petrov
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
class Program
{
    static FileSystemWatcher watcher;
    static int delay;
    static string path;
    static string zipPath;

    static void Main()
    {
        delay = 1000;
        path = "...";
        zipPath = "..."; // Path.GetTempFileName();

        watcher = new FileSystemWatcher(path);
        watcher.Created += OnChanged;
        watcher.EnableRaisingEvents = true;

        Console.ReadLine();
    }

    private static async void OnChanged(object sender, FileSystemEventArgs e)
    {
        watcher.EnableRaisingEvents = false;

        await Task.Delay(delay);

        ZipFile.CreateFromDirectory(path, zipPath);

        // Send zipPath

        File.Delete(zipPath);

        watcher.EnableRaisingEvents = true;
    }
}

При добавлении первого файла в отслеживаемую папку срабатывает событие.
В нём отключаем слежение за событиями.
Ждём некоторое время (delay).
Потом файлы из папки преобразуем в архив.
Архив посылаем клиенту.
После чего удаляем архив.
И снова включаем отслеживание событий.

Это лишь набросок кода.

→ Ссылка