C# file/folder helpers

Karen Payne - Apr 30 '23 - - Dev Community

There are times when the working with files and folders a developer runs into unusual task that for some may be difficult to figure out or takes too much time to code.

Bookmark this article for methods for working with files and folders and overtime more code samples will be added as there are many more to come. What will not be added are methods easy to find on the Internet.

Requires

  • Microsoft Visual Studio 2022 or higher
  • .NET Core 7 or higher

Is folder or file

When traversing a folder a developer may want to know if an item is a file or folder.

The following method will, if successful indicate if an item is a file or folder and if the item does not exists the method returns false.



public static (bool isFolder, bool success) IsFileOrFolder(string path)
{
    try
    {
        var attr = File.GetAttributes(path);
        return attr.HasFlag(FileAttributes.Directory) ? (true, true)! : (false, true)!;
    }
    catch (FileNotFoundException)
    {
        return (false, false);
    }
}


Enter fullscreen mode Exit fullscreen mode

In the following example, LogFile folder exists, its created using a msbuild task, Log does not exists but TextFile1.txt does exists.



static void Main(string[] args)
{
    var items = new List<string>
    {
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles"),
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log"),
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TextFile1.txt")
    };

    foreach (var item in items)
    {
        var (isFolder, success) = FileHelpers.IsFileOrFolder(item);
        if (success)
        {
            AnsiConsole.MarkupLine($"{item}");
            AnsiConsole.MarkupLine($"\tIs folder {isFolder}");
        }
        else
        {
            AnsiConsole.MarkupLine($"[white]{item}[/] [red]not found[/]");
        }
    }

    Console.ReadLine();
}


Enter fullscreen mode Exit fullscreen mode

Is file locked

The following method detects if a file is locked when a file is opened with the attribute FileShare.None. Typical example, a developer does not close an Excel file properly or has the Excel file open outside the project.

This will not work if a file is open in shared mode.



public static async Task<bool> CanReadFile(string fileName)
{
    static bool IsFileLocked(Exception exception)
    {
        var errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
        return errorCode is ErrorSharingViolation or ErrorLockViolation;
    }

    try
    {
        await using var fileStream = File.Open(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
        if (fileStream != null)
        {
            fileStream.Close();
        }
    }
    catch (IOException ex)
    {
        if (IsFileLocked(ex))
        {
            return false;
        }
    }

    await Task.Delay(50);

    return true;

}


Enter fullscreen mode Exit fullscreen mode

See the project IsFileLockedSample for an interactive example with an Excel file and a text file.

Natural sort file names

The following methods will sort file names that end in numbers or an array or list with strings ending in numbers the same as in Windows Explorer.



public class NaturalStringComparer : Comparer<string>
{

    [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string x, string y);

    public override int Compare(string x, string y)
        => StrCmpLogicalW(x, y);
}


Enter fullscreen mode Exit fullscreen mode

Example with a list of unsorted strings.



using System.Runtime.CompilerServices;
using Spectre.Console;
using static HelperLibrary.FileHelpers;

namespace NaturalSortSample;

internal class Program
{
    static void Main(string[] args)
    {
        StandardSortSample();
        NaturalSortSample();
        Console.ReadLine();
    }

    private static void NaturalSortSample()
    {
        Print("Natural sort");
        var fileNames = FileNames();

        fileNames.Sort(new NaturalStringComparer());

        foreach (var item in fileNames)
        {
            Console.WriteLine(item);
        }

    }
    private static void StandardSortSample()
    {
        Print("Standard sort");
        var fileNames = FileNames();

        foreach (var item in fileNames)
        {
            Console.WriteLine(item);
        }


    }

    private static List<string> FileNames() =>
        new()
        {
            "Example12.txt", "Example2.txt", "Example3.txt", "Example4.txt",
            "Example5.txt", "Example6.txt", "Example7.txt", "Example8.txt",
            "Example9.txt", "Example10.txt", "Example11.txt", "Example1.txt",
            "Example13.txt", "Example14.txt", "Example15.txt", "Example16.txt",
            "Example17.txt", "Example18.txt", "Example19.txt", "Example20.txt"
        };

    private static void Print(string text)
    {
        AnsiConsole.MarkupLine($"[yellow]{text}[/]");
    }

    [ModuleInitializer]
    public static void Init()
    {
        Console.Title = "Code sample";
        W.SetConsoleWindowPosition(W.AnchorWindow.Center);
    }
}


Enter fullscreen mode Exit fullscreen mode

Without the need for DLL import

Backend code



using System.Numerics;
using System.Text.RegularExpressions;

namespace NaturalSortSampleNew.Classes;
public static partial class Extensions
{
    public static IEnumerable<T> NaturalOrderBy<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
    {
        var regex = new Regex(@"\d+", RegexOptions.Compiled);

        int maxDigits = items
            .SelectMany(value => regex.Matches(selector(value))
                .Select(digitChunk => (int?)digitChunk.Value.Length)).Max() ?? 0;

        return items.OrderBy(value => regex.Replace(selector(value), 
            match => match.Value.PadLeft(maxDigits, '0')), 
            stringComparer ?? StringComparer.CurrentCulture);
    }

    public static IEnumerable<string> NaturalOrderBy(this IEnumerable<string> me)
    {
        return me.OrderBy(x => NumbersRegex().Replace(x, m => m.Value.PadLeft(50, '0')));
    }

    public static bool IsEven<T>(this T sender) where T : INumber<T>
        => T.IsEvenInteger(sender);

    [GeneratedRegex(@"\d+")]
    private static partial Regex NumbersRegex();
}



Enter fullscreen mode Exit fullscreen mode

Usage



private static void Example1()
{


    var fileNames = 
        """
        File1Test.txt
        File10Test.txt
        File2Test.txt
        File20Test.txt
        File3Test.txt
        File30Test.txt
        File4Test.txt
        File40Test.txt
        File5Test.txt
        File50Test.txt
        """.Split(Environment.NewLine);




    List<string> sorted = fileNames.NaturalOrderBy(x => x).ToList();
    sorted.ForEach(Console.WriteLine);

    var folder = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
    var files = Directory.EnumerateFiles(folder.FullName, "*.*", new EnumerationOptions
    {
        IgnoreInaccessible = true,
        RecurseSubdirectories = true
    }).ToArray();

    File.WriteAllLines("Documents.txt", files);

    Console.WriteLine();

    var fileInfoArray = fileNames.Select(x => new FileInfo(x)).ToArray();
    var sorted1 = fileInfoArray.NaturalOrderBy(x => x.Name).ToArray();

}


Enter fullscreen mode Exit fullscreen mode

Globbing

File globbing can in specific cases allow a developer to fine tune files that are looking for in a folder or folders.

Provides

  • Inclusion of files via patterns
  • Exclusion of file via patterns

Base patterns

Value Description
*.txt All files with .txt file extension.
*.* All files with an extension
* All files in top-level directory.
.* File names beginning with '.'.
*word* All files with 'word' in the filename.
readme.* All files named 'readme' with any file extension.
styles/*.css All files with extension '.css' in the directory 'styles/'.
scripts/*/* All files in 'scripts/' or one level of subdirectory under 'scripts/'.
images*/* All files in a folder with name that is or begins with 'images'.
**/* All files in any subdirectory.
dir/**/* All files in any subdirectory under 'dir/'.
../shared/* All files in a diretory named "shared" at the sibling level to the base directory

In this project (Razor Pages) the goal is to get all .cs file with several exclusions as per the variable exclude

await GlobbingOperations.Find(path, include, exclude); traverses the path which in this case is the current solution folder and on a match adds the match to a StringBuilder which we return the page and append to the body of the page the results.



public class IndexModel : PageModel
{
    public IndexModel()
    {
        GlobbingOperations.TraverseFileMatch += TraverseFileMatch;
    }

    [BindProperty]
    public StringBuilder Builder { get; set; } = new();
    public void OnGet()
    {

    }

    public async Task<PageResult> OnPost()
    {
        string path = DirectoryHelper.SolutionFolder();

        string[] include = { "**/*.cs", "**/*.cshtml" };
        string[] exclude =
        {
            "**/*Assembly*.cs",
            "**/*_*.cshtml",
            "**/*Designer*.cs",
            "**/*.g.i.cs",
            "**/*.g.cs",
            "**/TemporaryGeneratedFile*.cs"
        };

        Builder = new StringBuilder();
        await GlobbingOperations.Find(path, include, exclude);
        return Page();
    }

    private void TraverseFileMatch(FileMatchItem sender)
    {
        Log.Information(Path.Combine(sender.Folder, sender.FileName));
        Builder.AppendLine(Path.Combine(sender.Folder, sender.FileName));
    }
}


Enter fullscreen mode Exit fullscreen mode

Displaying images in a DataGridView

Note
Although the following is performed in a Windows Forms project the provided code can be adapted to other project types.

Using Globbing to iterate a folder and sub folders for specific .png files and display in a Windows Forms DataGridView.

Model



public class FileItem
{
    [Browsable(false)]
    public string Folder { get; set; }
    public string FileName { get; set; }
    [Browsable(false)]
    public byte[] Bytes { get; set; }
    public Image Image => Bytes.BytesToImage();
    public override string ToString() => Path.Combine(Folder, FileName);

}


Enter fullscreen mode Exit fullscreen mode

The attribute [Browsable(false)] is used so that those properties do not show in the DataGridView.

For displaying images in the DataGridView, we need to read bytes from each file, stored them in Bytes property which is used to create an Image using the following extension method.



public static class Extensions
{
    public static Image BytesToImage(this byte[] bytes)
    {
        var imageData = bytes;
        using MemoryStream ms = new(imageData, 0, imageData.Length);
        ms.Write(imageData, 0, imageData.Length);
        return Image.FromStream(ms, true);
    }
}


Enter fullscreen mode Exit fullscreen mode

The following code provides a method to iterate a given folder for specific image files using two arrays, one with patterns for what we want and another for files to exclude.

In the provided source, the code below is in a class project for reuse.



public class GlobbingOperations
{
    public delegate void OnTraverseFileMatch(FileMatchItem sender);
    public static event OnTraverseFileMatch TraverseFileMatch;
    public delegate void OnDone(string message);
    public static event OnDone Done;


    public static async Task GetImages(string parentFolder, string[] patterns, string[] excludePatterns)
    {

        Matcher matcher = new();
        matcher.AddIncludePatterns(patterns);
        matcher.AddExcludePatterns(excludePatterns);

        await Task.Run(() =>
        {

            foreach (string file in matcher.GetResultsInFullPath(parentFolder))
            {
                TraverseFileMatch?.Invoke(new FileMatchItem(file));
            }
        });

        Done?.Invoke("Finished");

    }
}


Enter fullscreen mode Exit fullscreen mode

In the form:

The following array indicates we want any files starting with Ri and Bl with an extension of .png to be included in the search operation.



string[] include = ["**/Ri*.png", "**/Bl*.png"];


Enter fullscreen mode Exit fullscreen mode

The following array indicated (similar to the above array) which files should not be included.



string[] exclude = ["**/blog*.png", "**/black*.png"];


Enter fullscreen mode Exit fullscreen mode

The following specifies the folder to perform the search on.



var folder = "C:\\Users\\paynek\\Documents\\Snagit";

if (!Directory.Exists(folder))
    return;


Enter fullscreen mode Exit fullscreen mode

Note Before running the code sample, change the folder location and the patterns for the two arrays.

Next, subscribe to an event for when a file is found and one for when the search is done.



GlobbingOperations.TraverseFileMatch += DirectoryHelpers_TraverseFileMatch;
GlobbingOperations.Done += DirectoryHelpers_Done;


Enter fullscreen mode Exit fullscreen mode

The events.

*Note *_files is a form level property.



private void DirectoryHelpers_TraverseFileMatch(FileMatchItem sender)
{
    var item = new FileItem() { Folder = sender.Folder, FileName = sender.FileName };
    item.Bytes = File.ReadAllBytes(Path.Combine(item.Folder,item.FileName));
    _files.Add(item);
}
private void DirectoryHelpers_Done(string message)
{
    _files = _files.OrderBy(x => x.FileName).ToList();
}


Enter fullscreen mode Exit fullscreen mode

Next, perform the search.



await GlobbingOperations.GetImages(folder, include,exclude);


Enter fullscreen mode Exit fullscreen mode

Sample project

Shows DataGridView with images and file names

How to create auto-incrementing file names

A developer may need to create file names that increment e.g. file1.txt, file2.txt etc.

The code presented shows how to in DirectoryHelpersLibrary.GenerateFiles For use in your application

  • Copy GenerateFiles.cs to your project
  • Change property _baseFileName to the base file name for creating, currently is set to data.
  • Change property _baseExtension to the file extension you want.

In the code sample each time the program runs it creates three .json files.

First time, Data_1.json, Data_2.json and Data_3.json
Second time, Data_4.json, Data_5.json and Data_6.json

And so on.

Provided code sample



static void Main(string[] args)
{
    AnsiConsole.MarkupLine(GenerateFiles.HasAnyFiles()
        ? $"Last file [cyan]{Path.GetFileName(GenerateFiles.GetLast())}[/]"
        : "[cyan]No files yet[/]");

    JsonSerializerOptions options = JsonSerializerOptions();
    AnsiConsole.MarkupLine("[white on blue]Create files[/]");
    foreach (var person in MockedData.PeopleMocked())
    {
        var (success, fileName) = GenerateFiles.CreateFile();
        if (success)
        {
            AnsiConsole.MarkupLine($"   [white]{Path.GetFileName(fileName)}[/]");
            File.WriteAllText(fileName, JsonSerializer.Serialize(person, options));
        }
        else
        {
            AnsiConsole.MarkupLine($"[red]Failed to create {fileName}[/]");
        }


    }

    Console.WriteLine();
    AnsiConsole.MarkupLine($"[white]Next file to be created[/] {Path.GetFileName(GenerateFiles.NextFileName())}");

    AnsiConsole.MarkupLine("[cyan]Done[/]");
    Console.ReadLine();
}


Enter fullscreen mode Exit fullscreen mode
Method Description
CreateFile Generate next file in sequence
RemoveAllFiles Removes files generated
HasAnyFiles Provides a count of generated files
NextFileName Get next file name without creating the file
GetLast Get last generated file name

Stay tune for more

More useful methods will be added over time.

Source code

Clone the following GitHub repository.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .