C# - Directory Enumerator

Example/Tutorials

This tutorial helps demonstrate a Directory Enumerator.

Getting Started - Picking The Project

  1. Windows Classic Desktop Project
  2. Console App (.NET Framework)


As an additional two steps, I like to convert Main to Async/Await out the gate. Guide to creating an Async/Await Main(string[] args). I also like to debug/run as admin so... Guide to making app start as Administrator.

Looking At Directory Enumerator

This is good starting place for us to write a test method getting all directories from a folder.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace DirectoryTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            await Console.Out.WriteLineAsync("I've turned myself Asynchronous, Morty!");
            await Console.In.ReadLineAsync();
        }
    }
}

The Steps

Setting The Root Folder

Nothing much to it, just set a local or global variable to the folder or Drive you want to search.

    class Program
    {
        static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            string rootFolder = "C:\\";
            await Console.In.ReadLineAsync();
        }
    }

Lets go ahead and also create a secondary method so we don't clutter up MainAsync. This example will do the majority of work now in the DirectoryTestAsync method.

        static async Task MainAsync(string[] args)
        {
            string rootFolder = "C:\\";
            await DirectoryTestAsync(rootFolder);
            await Console.In.ReadLineAsync();
        }

        static async Task DirectoryTestAsync(string rootFolder)
        {

        }

Let us setup the Test itself, not the actual code that will be called. Below, we will have a start and stop message. I will want to see how long the method tanks, so my WORK function will be returning a double representing seconds. I also want to repeat any test an x number of times. Below it is set to 5 (remember the test count starts at 0). I want to pass in the current test count for display purposes.

        static async Task DirectoryTestAsync(string rootFolder)
        {
            await Console.Out.WriteLineAsync($"Enumerate Directories Test Begins.");
            var seconds = 0.0;
            var limit = 5;
            for (int i = 0; i < limit; i++)
            {
                //seconds += await WORKASYNC(rootFolder, i);
            }
            await Console.Out.WriteLineAsync(
                    $"Enumerate Directories Test Complete.\n\tTotal Time: {seconds}s\tAvg. Time: {seconds / limit}s\n");
        }

Up next, we need to define a WORKASYNC function and even name it properly and set the Type for the Return Task Result (double).

        static async Task DirectoryTestAsync(string rootFolder)
        {
            await Console.Out.WriteLineAsync($"Enumerate Directories Test Begins.");
            var seconds = 0.0;
            var limit = 5;
            for (int i = 0; i < limit; i++)
            {
                seconds += await EnumerateDirectoriesAsync(rootFolder, i);
            }
            await Console.Out.WriteLineAsync(
                    $"Enumerate Directories Test Complete.\n\tTotal Time: {seconds}s\tAvg. Time: {seconds / limit}s\n");
        }

        static async Task<double> EnumerateDirectoriesAsync(string rootFolder, int iteration)
        {

        }

Okay, we should see a compiler error from IntelliSense. We need to return a Task<double> but we have a little more complexity due to the fact that this is Asynchronous calls, and unfortunately the calls we are about to make are Synchronous. This is one way of maintaining your Task Async/Await pattern when you run into Synchronous code functions that you HAVE to call.

        static async Task<double> EnumerateDirectoriesAsync(string rootFolder, int iteration)
        {
            var t = await Task.Run(() =>
            {
                //ACTION(s) GO HERE
            });

            return t;
        }

We still have a compile error because we are not returning a (double) inside the Action block scope of the makeshift action.

        static async Task<double> EnumerateDirectoriesAsync(string rootFolder, int iteration)
        {
            var t = await Task.Run(() =>
            {
                //ACTION(s) GO HERE
                return 0.0;
            });

            return t;
        }

Now we compile but we don't do anything. We need to add one of our recursive methods that call a builtin NET method now to the action block of EnumerateDirectoriesAsync(); We will also be converting our Action block to async as well by changing the Task.Run line to Task.Run( async () => { /*ActionBlock*/ };

        static async Task<double> EnumerateDirectoriesAsync(string rootFolder, int iteration)
        {
            var t = await Task.Run( async () =>
            {
                await Console.Out.WriteLineAsync($"\tTest {iteration}: Finding Directories in ({rootFolder}) Begins...");
                Stopwatch sw = Stopwatch.StartNew();

                var dirs = new List<string>();
                dirs = EnumerateDirectories(rootFolder, "*", SearchOption.AllDirectories).ToList();
                sw.Stop();
                var seconds = sw.ElapsedMilliseconds / 1000.0;

                await Console.Out.WriteLineAsync($"\t\t== Test {iteration} Summary ==");
                await Console.Out.WriteLineAsync($"\t\t   Directories Enumerated: {dirs.Count}");
                await Console.Out.WriteLineAsync($"\t\t   Elapsed Time: {seconds}s");

                return seconds;
            });

            return t;
        }

This block of code utilizes System.Collections.Generic, System.Linq, System.IO, and System.Diagnostics. Be sure you have those references that were included at the beginning of the tutorial. The only thing unaccounted for is the EnumerateDirectories method we will have to write that.

        static async Task<double> EnumerateDirectoriesAsync(string rootFolder, int iteration)
        {
            var t = await Task.Run( async () =>
            {
                await Console.Out.WriteLineAsync($"\tTest {iteration}: Finding Directories in ({rootFolder}) Begins...");
                Stopwatch sw = Stopwatch.StartNew();

                var dirs = new List<string>();
                dirs = EnumerateDirectories(rootFolder, "*", SearchOption.AllDirectories).ToList();
                sw.Stop();
                var seconds = sw.ElapsedMilliseconds / 1000.0;

                await Console.Out.WriteLineAsync($"\t\t== Test {iteration} Summary ==");
                await Console.Out.WriteLineAsync($"\t\t   Directories Enumerated: {dirs.Count}");
                await Console.Out.WriteLineAsync($"\t\t   Elapsed Time: {seconds}s");

                return seconds;
            });

            return t;
        }

        static IEnumerable<string> EnumerateDirectories(string parentDirectory, string searchPattern,
                                                        SearchOption searchOpt)
        {
            var result = Enumerable.Empty<string>();
            try
            {
                var directories = Enumerable.Empty<string>();
                if (searchOpt == SearchOption.AllDirectories)
                {
                    directories = Directory.EnumerateDirectories(parentDirectory)
                                           .SelectMany(x => EnumerateDirectories(x, searchPattern, searchOpt));
                }
                result = directories.Concat(Directory.EnumerateDirectories(parentDirectory, searchPattern));
            }
            catch (UnauthorizedAccessException) { }

            return result;
        }

Now lets disect the EnumerateDirectories call. This is a synchronous method and spoiler alert, a very long running method. That's why it's wrapped inside the async action block above. This is also a recursive call. It's going to call itself for every subdirectory, every subsubdirectory, every subsubsubdirectory. et cetera. This call has the potential for a StackOverflow depending on how deep your folder structure gets and how many subfolders are in each folder etc. You also have the risk of not having the correct Access permission or User permissions, both which can lead to exceptions that can blow up your stack if you are not careful.

Recursion

Recursion can be a tricky concept, but your stack consists of N number of calls, one for every directory. To do that, the function calls itself once it acquires all the subdirectories, passing in the subdirectory as the new root. Each stack, when finished, returns a concatenation all the way up the stack till you reach the parent function. That outputs an IEnumerable<string> that has all directories in it.

Understanding what is happening is super important because this is not the best way to write this code. Recursion is rarely, save some math examples, the best route to go.

Sources