Introduction
Microsoft has called me a few times in the past for an interview. Once to work for the ASP.NET team and once to work for what used to be called the national practices services, part of Microsoft Consulting, and for a while I worked as a consultant for Microsoft consulting services.
Microsoft is building software for general consumption, or thinking of it another way, for the widest audience possible. Part of my interview of the ASP.NET was how to build software for general consumption. The challenge with general consumption software is that a developer may want to make things easier, but easy software sometimes opens some doors and closes others. I think this is what happened with Directory.GetFiles
and Directory.GetDirectories
.
GetFiles
and GetDirectories
replaced the API methods findfirst
and findnext
to make traversing the file system easier. The drawback with GetFiles
and GetDirectories
was that these methods returned arrays, and the code had to wait until the entire array was populated before the calls returned. To eliminate the wait GetFiles
and GetDirectories
have newer versions named EnumerateFiles
and EnumerateDirectories
. You no longer have to wait for EnumerateFiles
and EnumerateDirectories
to return to begin using the returned files and directories, but there are a couple of minor obstacles involved here to.
With EnumerateFiles
and EnumerateDirectories
you don’t have to map API calls, write your own recursive descent to map items in sub-folders, and you don’t have to wait for the entire enumeration to finish to interact with the results. The obstacle is that if you encounter an error, for example when you enumerate folders and child folders, the EnumerateXxx
method will throw an exception, and the enumeration will fail. This makes it difficult to do something as seemingly simple as create a file system browser with these methods. There is hope though.
Let’s take a look at EnumerateFiles
and EnumerateDirectories
, how to use LINQ, and exception handlers to walk the file system even in the face of access exceptions.
Using EnumerateFiles and EnumerateDirectories
A version of EnumerateFiles
and EnumerateDirectories
exists in both the Directory and DirectoryInfo classes in the System.IO namespace. The version in the Directory class can traverse sub-directories and returns just the names of the matched items. If you need the file or directory information then use the DirectoryInfo version. Both EnumerateFiles
and EnumerateDirectories
have several overloaded versions that accept a variation of arguments including the starting path, the file mask, and a SearchOption
enumeration argument that determines whether just the top folder or all folders are searched.
The following code uses Directory.EnumerateFiles
to return the files in the root of the C: drive. The resultant enumeration is used as a source in a LINQ query. (In this case it is worth noting that the LINQ query doesn’t filter the data so it is superfluous; that is you could just use Directory.EnumeratoryFiles
by itself.) The Array.ForEach
method converts the enumeration to an array, a requirement of the ForEach
method, and a Lambda expression to write the results to the console.
Dim files = From file In Directory.EnumerateFiles("C:") Select file Array.ForEach(files.ToArray(), Sub(f) Console.WriteLine(f))
In such a simple use case EnumerateFiles
and EnumerateDirectories
are easy to use. This is what we want from a framework.
Handling EnumerateDirectories Exceptions
If you call Directory.EnumerateDirectories
with the SearchOption.AllDirectories
argument then EnumerateDirectories
will perform a directory traversal for you, which includes sub-folders. However, if you start at a high level directory like the root then you are likely to encounter a folder that will cause an access exception. You won’t know this until you touch the directory data though. For example, if you write:
Dim d = Directory.EnumerateDirectories("C:", "*.*", SearchOption.AllDirectories)
Then the code will compile and run. If you use the results as the source for a for loop
such as the following:
' Get all directories For Each d In Directory.EnumerateDirectories("C:", "*.*", SearchOption.AllDirectories) Console.WriteLine(d) Next
Then the code may throw an UnauthorizedAccessException
when it hits a folder like C:Documents and Settings. To catch the exception wrap the for loop
in a Try Catch
block. Here is the revised code.
Try For Each d In Directory.EnumerateDirectories("C:", "*.*", SearchOption.AllDirectories) Console.WriteLine(d) Next Catch ex As Exception Console.WriteLine("Handle exception here") End Try
You can use a broader exception class like Exception to catch all possible exceptions or multiple catch blocks if you want to handle individual kinds of exceptions differently.
A potential challenge here is that if you actually want to display or capture all folders even if access is denied then you need a slightly different approach than simply using the SearchOption.AllDirectories
argument.
Searching All Folders Without Failing on Exceptions
Suppose you want to traverse all folders. Suppose further that if you encounter an exception you want to skip that folder and continue. Until there is an option like SearchOption.ContinueOnErrors
or an attachable event that performs a semantically similar operation there is no opportunity to continue on an exception when it only takes one method call to traverse sub-folders. For this reason you need to split the call up.
To effectively skip folders and continue you can combine a Stack object and obtain top level folders. Next, pop each folder off the stack and try to query the sub-folders, repeat the pushing and popping of the stack until all folders and sub-folders have been traversed. Because this approach splits the traversal into an outer loop to manage the stack you can handle an exception and continue processing the items in the stack. Listing 1 provides a solution that will grab unauthorized folders-for example for your file system management tool-too and continue processing other folders.
Imports System.IO Imports System.Security Module Module1 Sub Main() Dim results As List(Of String) = New List(Of String) Dim start As String = "c:" results.Add(start) Dim stack As Stack(Of String) = New Stack(Of String) Do Try Debug.WriteLine(start) Dim dirs = From d In Directory.EnumerateDirectories(start, "*.*", SearchOption.TopDirectoryOnly) Select d ' multline Lambda - don't really need this Array.ForEach(dirs.ToArray(), Sub(d) stack.Push(d) End Sub) start = stack.Pop() results.Add(start) Catch ex As UnauthorizedAccessException Console.WriteLine(ex.Message) start = stack.Pop() results.Add(start) End Try Loop Until (stack.Count = 0) For Each d In results Console.WriteLine(d) Next Console.ReadLine() End Sub End Module
Listing 1: Using a Stack to track top-level folders and then process sub-folders one at a time in the event of an exception.
The List(Of String) will contain the found directories. The variable start will start the search at the root of the C: drive. The stack is used to store the directories that need to be searched. The Do
loop starts the process and the Try
begins the exception handling block.
The statement beginning with Dim dirs
begins searching the sub-folders in the folder assigned to start. SearchOption.TopDirectoryOnly
will return the folders in start. (You could skip the LINQ query if you didn’t want to perform an additional filtering, but the code does demonstrate how to incorporate LINQ if you need to.) The Array.ForEach
pushes all of the child folders on the stack using multi-line Lambda sub-expression syntax. Next, the stack is popped, the next folder is stored in the results list, and the process continues if the stack is not empty. If an exception is thrown the stack is popped again and the next item is stored.
The way this solution is written and by starting at the root this will be a long running process. However, since the solution is broken up into multiple stages you have plenty of opportunities to interact with the results along the way and you can even split the solution into a background process–which is probably what Windows Explorer does.
Summary
EnumerateFiles
and EnumerateDirectories
make it really easy to get file and directory names and information. The most common complaint on the web is that these methods stop processing on an exception. By splitting the call up by directories you can manage exceptions in an outer loop. The online response to aborted processing is to modify the EnumerateXxx
methods to incorporate continue behavior.