FileSystemWatcher monitora um diretório e dispara um evento quando um arquivo dentro dele é criado, excluído, renomeado ou gravado. O sistema operacional detecta cada alteração, e a classe é a camada do .NET que repassa essa alteração para nós.

Filter define de quais arquivos recebemos aviso, e NotifyFilter define os tipos de alteração. Nenhum evento é disparado enquanto EnableRaisingEvents não for definido como true.

Dois comportamentos pegam muita gente de surpresa quando o FileSystemWatcher começa a rodar: salvar um arquivo uma única vez pode gerar três eventos, e um diretório movimentado pode gerar menos eventos do que deveria.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

O que é o FileSystemWatcher em C#?

FileSystemWatcher é uma classe do namespace System.IO que monitora um diretório e dispara um evento sempre que algo dentro dele muda. Ela não fica consultando o disco: o sistema operacional informa cada alteração, e a classe a entrega para nós.

Três configurações decidem do que somos avisados. Path é o diretório a monitorar. Filter restringe o monitoramento a um padrão de arquivo, como *.txt. NotifyFilter decide quais tipos de alteração contam, do nome de um arquivo até as configurações de segurança dele.

A classe dispara cinco eventos: Created, Deleted, Changed, Renamed e Error. Nenhum deles é disparado até definirmos EnableRaisingEvents como true.

O FileSystemWatcher funciona em um disco local ou em um compartilhamento de rede, e passa a monitorar também os subdiretórios quando ativamos essa opção com IncludeSubdirectories. Ele nunca nos diz quem fez a alteração nem o que o arquivo contém agora, apenas que um caminho que nos interessa apareceu, sumiu, foi renomeado ou foi gravado.

Vejamos como configurar o FileSystemWatcher no projeto:

public class TextFileManager
{
    private readonly FileSystemWatcher _fileSystemWatcher;

    public TextFileManager(string rootDirectory)
    {
        _fileSystemWatcher = new FileSystemWatcher(rootDirectory);
    }
}

Podemos inicializar FileSystemWatcher simplesmente passando o caminho que queremos monitorar. Ao longo deste artigo, vamos usar a classe TextFileManager para configurar a instância de FileSystemWatcher e trabalhar com ela. Claro, você sempre pode baixar o nosso código-fonte para ver a implementação completa.

O código de exemplo provoca os eventos com chamadas comuns, como File.WriteAllLines(), File.Move() e File.Delete(), e o nosso artigo sobre as classes File e Directory explica essas chamadas em detalhes.

Como filtrar as notificações do FileSystemWatcher?

Duas propriedades fazem a filtragem, e cada uma responde a uma pergunta diferente. Filter decide quais arquivos monitoramos. NotifyFilter decide quais alterações nesses arquivos chegam até nós.

Filter recebe um único padrão com curingas, e o valor padrão é *, que monitora todos os arquivos. Defini-la como *.txt limita o FileSystemWatcher a arquivos de texto. Para vários padrões ao mesmo tempo, existe Filters, uma coleção, de modo que uma única instância pode monitorar *.txt e *.log sem precisarmos criar uma segunda.

NotifyFilter recebe uma combinação de valores de NotifyFilters feita com OR bit a bit, e já vem configurada: o valor padrão é a combinação de LastWrite, FileName e DirectoryName. Acrescentar Size ou Attributes amplia o monitoramento, e deixar um valor de fora o restringe.

Um monitoramento restrito também é o mais seguro. Cada notificação que o FileSystemWatcher aceita ocupa espaço em um buffer interno de tamanho fixo, então cada valor extra de NotifyFilters e cada subdiretório extra tornam mais provável um estouro e, com ele, a perda de um evento.

Filter

Filter é uma propriedade de FileSystemWatcher que nos permite monitorar arquivos específicos informando um padrão de arquivo. O valor padrão de Filter é *, que monitora todos os arquivos, e *.* ou uma string vazia têm o mesmo efeito.

Vamos configurar o tipo de arquivo que queremos monitorar:

private void ConfigureFilters()
{
    _fileSystemWatcher.Filter = "*.txt";
}

Agora _fileSystemWatcher monitora apenas alterações em arquivos de texto.

Definir Filter coloca o padrão na coleção Filters, então adicionar *.log a Filters depois faz com que a mesma instância informe alterações em arquivos de log, além dos arquivos de texto.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

NotifyFilter

NotifyFilter é outra propriedade de FileSystemWatcher que nos permite especificar e monitorar alterações específicas no sistema de arquivos. Podemos configurar NotifyFilter para escutar vários tipos de alteração usando o operador OR bit a bit ("|"). Por padrão, o valor de NotifyFilter é uma combinação OR bit a bit de LastWrite, DirectoryName e FileName, mas podemos ampliá-la:

private void ConfigureFilters()
{
    ...
    _fileSystemWatcher.NotifyFilter = NotifyFilters.Attributes
                                | NotifyFilters.CreationTime
                                | NotifyFilters.DirectoryName
                                | NotifyFilters.FileName
                                | NotifyFilters.LastAccess
                                | NotifyFilters.LastWrite
                                | NotifyFilters.Security
                                | NotifyFilters.Size;
}

Combinando valores de NotifyFilters, ampliamos o leque de alterações monitoradas. A enumeração tem oito valores, e a última coluna mostra quais três já fazem parte do valor padrão:

Valor de NotifyFiltersAlteração que informaFaz parte do padrão?
FileNameO nome do arquivoSim
DirectoryNameO nome do diretórioSim
LastWriteA data da última gravação no arquivo ou na pastaSim
AttributesOs atributos do arquivo ou da pastaNão
SizeO tamanho do arquivo ou da pastaNão
LastAccessA data da última abertura do arquivo ou da pastaNão
CreationTimeA hora de criação do arquivo ou da pastaNão
SecurityAs configurações de segurança do arquivo ou da pastaNão

Também podemos monitorar alterações dentro de subdiretórios com a propriedade IncludeSubdirectories:

private void ConfigureFilters()
{
    ...
    _fileSystemWatcher.IncludeSubdirectories = true;
    ...
}

Quais eventos o FileSystemWatcher dispara?

FileSystemWatcher tem 5 tipos de evento diferentes que podemos usar. São eles: Created, Deleted, Changed, Renamed e Error.

Os cinco manipuladores a seguir vêm da classe TextFileManager do exemplo, e nenhum deles entra em ação enquanto EnableRaisingEvents não for definido, algo que o último trecho de código desta seção faz.

Created

Podemos configurá-lo para disparar um evento na criação de um arquivo ou de um diretório:

private void ConfigureEvents()
{
    _fileSystemWatcher.Created += HandleCreated;
}

private void HandleCreated(object sender, FileSystemEventArgs e)
{
    Console.WriteLine($"Create: {e.FullPath}");
}

Deleted

Podemos configurá-lo para disparar um evento na exclusão de um arquivo ou diretório:

private void ConfigureEvents()
{
    _fileSystemWatcher.Deleted += HandleDeleted;
    ...
}

private void HandleDeleted(object sender, FileSystemEventArgs e)
{
    Console.WriteLine($"Delete: {e.FullPath}");
}

Changed

Podemos configurá-lo para disparar um evento quando há uma alteração no tamanho, nos atributos de sistema, na última atualização, na hora do último acesso ou nas permissões de um arquivo ou diretório:

private void ConfigureEvents()
{
    _fileSystemWatcher.Changed += HandleChanged;
    ...
}

private void HandleChanged(object sender, FileSystemEventArgs e)
{
    Console.WriteLine($"Change: {Enum.GetName(e.ChangeType)} {e.FullPath}");
}

Os manipuladores de eventos de Created, Deleted e Changed têm assinaturas de método parecidas.

Renamed

Podemos usá-lo para disparar um evento quando o nome de um arquivo ou de um diretório muda:

private void ConfigureEvents()
{
    _fileSystemWatcher.Renamed += HandleRenamed;
   ...
}

private void HandleRenamed(object sender, RenamedEventArgs e)
{
    Console.WriteLine($"Rename: {e.OldName} => {e.Name}");
}

Os manipuladores do evento Renamed têm uma assinatura de método diferente da dos manipuladores de Created, Deleted e Changed. O segundo argumento, do tipo RenamedEventArgs, contém as propriedades com o nome antigo e o nome novo do arquivo ou diretório afetado.

Error

Por diversos motivos, FileSystemWatcher pode chegar a um estado em que não consegue mais monitorar as alterações do sistema de arquivos no caminho especificado. Para disparar um evento nessa situação, usamos um manipulador para o evento Error:

private void ConfigureEvents()
{
    _fileSystemWatcher.Error += HandleError; 
    ...
}

private void HandleError(object sender, ErrorEventArgs e)
{
    Console.WriteLine($"Error: {e.GetException().Message}");
}

Por fim, para que _fileSystemWatcher dispare eventos, precisamos definir a propriedade EnableRaisingEvents como true:

private void ConfigureEvents()
{
    ...
    _fileSystemWatcher.EnableRaisingEvents = true;
}

Para começar o monitoramento, chamamos ConfigureFilters() e depois ConfigureEvents() no fim do construtor de TextFileManager, logo depois de criar o FileSystemWatcher.

Os eventos de FileSystemWatcher são disparados conforme as alterações correspondentes no sistema de arquivos dentro do caminho monitorado e de acordo com os filtros. Aqui estão os cinco eventos juntos, com o argumento que cada manipulador recebe:

EventoArgumento do manipuladorDisparado quando
CreatedFileSystemEventArgsUm arquivo ou diretório que atende aos filtros aparece no caminho monitorado
DeletedFileSystemEventArgsUm arquivo ou diretório que atende aos filtros é removido
ChangedFileSystemEventArgsO conteúdo, o tamanho, os atributos, os timestamps ou as configurações de segurança de um item que atende aos filtros mudam
RenamedRenamedEventArgs (acrescenta OldName e OldFullPath)Um arquivo ou diretório que atende aos filtros é renomeado
ErrorErrorEventArgs (chame GetException())O FileSystemWatcher perde o rastro das alterações, normalmente por causa de um estouro de buffer, ou para de monitorar porque o próprio diretório monitorado desapareceu

Operações comuns no sistema de arquivos, como mover ou copiar, podem disparar mais de um evento.

Por que o FileSystemWatcher dispara vários eventos para uma única alteração?

Porque uma ação no sistema de arquivos raramente é uma única operação no disco. Salvar um arquivo em um editor pode gravar os bytes, atualizar o timestamp e mexer nos atributos, e cada uma dessas etapas é uma notificação separada.

A Microsoft documenta esse comportamento. Mover um arquivo entre diretórios pode disparar vários eventos Changed, junto com Created e Deleted, porque uma movimentação é composta de várias operações mais simples. Outros softwares na máquina também geram eventos, principalmente os antivírus, e o FileSystemWatcher informa essa atividade como qualquer outra.

Por isso, os manipuladores precisam tolerar repetições. Uma forma é guardar o último caminho e o timestamp que processamos e ignorar uma repetição dentro de um intervalo curto. Outra é colocar cada evento em uma fila e deixar que um único consumidor decida o que vale a pena fazer.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Created também é disparado assim que o arquivo aparece, e não quando quem está gravando nele termina, então lê-lo imediatamente pode esbarrar em um arquivo bloqueado ou gravado pela metade.

Fluxo que vai de uma alteração no sistema de arquivos até os cinco eventos do FileSystemWatcher, passando pela notificação do sistema operacional e pelo buffer interno de 8 KB da classe, com um ramo de estouro em que as notificações são descartadas e o evento Error é disparado.

O buffer interno é o único ponto desse fluxo em que as notificações podem se perder.

A documentação do FileSystemWatcher diz isso com todas as letras: “Operações comuns no sistema de arquivos podem disparar mais de um evento.” O exemplo dado é um arquivo movido de um diretório para outro, o que, segundo a documentação, pode disparar “vários eventos OnChanged e alguns eventos OnCreated e OnDeleted“.

Antes de ler um arquivo do qual acabou de ser avisado, o manipulador pode verificar se o arquivo ainda está em uso e tentar de novo mais tarde se o processo que o grava ainda o mantiver bloqueado.

Na abordagem com fila, podemos entregar cada evento a uma fila com os Channels do .NET: o manipulador grava o evento no canal e retorna na hora, e um único consumidor lê o canal no próprio ritmo.

O caminho em FileSystemEventArgs não diz se indica um arquivo ou um diretório. Um manipulador que precisa saber isso tem de verificar por conta própria se um caminho é um arquivo ou um diretório, o que só funciona enquanto o caminho ainda existe.

Como evitar que o FileSystemWatcher perca eventos?

Impedindo que o buffer interno estoure. O sistema operacional guarda as notificações de alteração em um buffer que o FileSystemWatcher cria. O valor padrão de InternalBufferSize é 8.192 bytes, e cada notificação ocupa até 16 bytes mais o nome do arquivo. Quando o buffer enche, as notificações se perdem e o FileSystemWatcher dispara Error.

Quatro medidas evitam que ele encha. Restrinja NotifyFilter às alterações que o código realmente trata. Deixe IncludeSubdirectories desativado, a menos que seja necessário. Prefira nomes de arquivo curtos no diretório monitorado, porque o nome é a parte de cada notificação que varia de tamanho. Mantenha os manipuladores curtos e passe o trabalho lento para outra thread.

Aumente InternalBufferSize só depois dessas quatro medidas. A Microsoft documenta 4 KB como mínimo e 64 KB como máximo, e a propriedade arredonda silenciosamente para 4.096 qualquer valor abaixo de 4.096 bytes. O buffer também vem da memória não paginada, que não pode ser transferida para o disco, então um buffer maior tem um custo real para a máquina.

Para evitar o estouro do buffer:

  • Reduza ao mínimo os tipos de alteração monitorados, com NotifyFilter e IncludeSubdirectories
  • Monitore apenas os arquivos de interesse, especificando o padrão de arquivo. Evite monitorar arquivos com nomes longos, porque cada notificação ocupa até 16 bytes mais o nome do arquivo
  • Reduza ao mínimo o código dos manipuladores de eventos
  • Se ainda não houver espaço suficiente, aumente o tamanho do buffer definindo a propriedade InternalBufferSize
  • Trate o evento Error. Uma InternalBufferOverflowException significa que notificações se perderam enquanto o FileSystemWatcher continuava funcionando, mas, se EnableRaisingEvents tiver mudado para false, como acontece quando o diretório monitorado é excluído, recrie o FileSystemWatcher

Conclusão

FileSystemWatcher precisa de três propriedades para informar eventos do sistema de arquivos (Path, Filter e NotifyFilter), além de EnableRaisingEvents para ser ativado.

O resto do trabalho está nos manipuladores. Eles devem presumir que toda alteração pode chegar mais de uma vez e que um arquivo recém-criado ainda não está pronto para leitura, e o monitoramento deve continuar restrito para que o buffer não estoure e descarte notificações.

Um FileSystemWatcher só dispara eventos até ser descartado, então, em uma aplicação de longa duração, podemos executá-lo dentro de uma tarefa em segundo plano que fique responsável por ele enquanto o host estiver em execução.

Testado com .NET 10.0.10.