2
\$\begingroup\$

What do you think about this file system manipulation helper? There is an utility class Folder which I can use to define directory structure of my app:

static class Folders
{
    public static Folder Bin =>
        new Folder(Assembly.GetExecutingAssembly());

    public static Folder App => Bin.Up();
    public static Folder Docs => App.Down("Docs");
    public static Folder Temp => App.Down("Temp");
}

Then I can do some manipulations in an easy way:

    static void Main(string[] args)
    {
        Folders.Temp.Create();
        Folders.Bin.Run("scan.exe.", "-no-ui");
        Folders.Temp.CopyTo(Folders.Docs, f => f.EndsWith(".pdf"));
        Folders.Temp.Empty();
    }

Here is the library class Folder used above:

public class Folder
{
    readonly string _path;

    public Folder(Assembly assembly) 
        : this(Path.GetDirectoryName(assembly.Location))
    {
    }

    public Folder(string path)
    {
        _path = path;
    }

    public override string ToString() => _path;

    public static implicit operator string(Folder folder) =>
        folder.ToString();

    public IEnumerable<Folder> Folders =>
        Directory.GetDirectories(this, "*", SearchOption.TopDirectoryOnly)
            .Select(p => new Folder(p));

    public IEnumerable<Folder> AllFolders =>
        Directory.GetDirectories(this, "*", SearchOption.AllDirectories)
            .Select(p => new Folder(p));

    public IEnumerable<string> Files =>
        Directory.GetFiles(this, "*.*", SearchOption.TopDirectoryOnly);

    public IEnumerable<string> AllFiles =>
        Directory.GetFiles(this, "*.*", SearchOption.AllDirectories);

    public Folder Up() =>
        new Folder(
            new DirectoryInfo(this)
                .Parent.FullName);

    public Folder Down(string folderName) =>
        new Folder(
            Path.Combine(
                this,
                folderName));

    public void Create() =>
        Directory.CreateDirectory(this);

    public void Empty()
    {
        var directoryInfo = new DirectoryInfo(this);
        if (!directoryInfo.Exists)
            return;

        foreach (var file in AllFiles)
            File.Delete(file);

        foreach (var folder in Folders)
            Directory.Delete(folder, true);
    }

    public void CopyTo(Folder destination) =>
        CopyTo(destination, file => true);

    public void CopyTo(Folder destination, Func<string, bool> filter)
    {
        //Create directories
        foreach (string directoryPath in AllFolders)
            Directory.CreateDirectory(directoryPath.Replace(this, destination));

        //Copy all the files & replaces any files with the same name
        foreach (string filePath in AllFiles.Where(filter))
            File.Copy(filePath, filePath.Replace(this, destination), true);
    }

    public void Run(string exe, string args = "")
    {
        string defaultCurrentDirectory = Environment.CurrentDirectory;
        Environment.CurrentDirectory = this;
        try
        {
            var process = new Process();
            process.StartInfo.FileName = exe;
            process.StartInfo.Arguments = args;
            process.Start();
            process.WaitForExit();
            if (process.ExitCode != 0)
                throw new InvalidOperationException(
                    $"{exe} process failed with exit code {process.ExitCode}.");
        }
        finally
        {
            Environment.CurrentDirectory = defaultCurrentDirectory;
        }
    }
}
\$\endgroup\$
3
\$\begingroup\$

I'm always a fan of using interfaces wherever possible so that unit tests can mock out my component easily. So let's create a couple of interfaces:

public interface IFolder
{
    IEnumerable<IFolder> Folders { get; }

    IEnumerable<IFolder> AllFolders { get; }

    IEnumerable<string> Files { get; }

    IEnumerable<string> AllFiles { get; }

    IFolder Up();

    IFolder Down(string folderName);

    void Create();

    void Empty();

    void CopyTo(IFolder destination);

    void CopyTo(IFolder destination, Func<string, bool> filter);

    void Run(string exe, string args = "");
}

and

internal interface IFolders
{
    IFolder Bin { get; }

    IFolder App { get; }

    IFolder Docs { get; }

    IFolder Temp { get; }
}

I also like to re-use constants. And I added another implicit operator to go with your new constructor:

public class Folder : IFolder
{
    private const string DirectoryWildcard = "*";

    private const string FileWildcard = "*.*";

    private readonly string _Path;

    public Folder(Assembly assembly)
        : this(Path.GetDirectoryName(assembly.Location))
    {
    }

    public Folder(string path)
    {
        this._Path = path;
    }

    public override string ToString() => this._Path;

    public static implicit operator string(Folder folder) => folder.ToString();

    public static implicit operator Folder(string path) => new Folder(path);

    public static implicit operator Folder(Assembly assembly) => new Folder(assembly);

    public IEnumerable<IFolder> Folders => Directory
        .GetDirectories(this, DirectoryWildcard, SearchOption.TopDirectoryOnly)
        .Select(path => new Folder(path));

    public IEnumerable<IFolder> AllFolders => Directory
        .GetDirectories(this, DirectoryWildcard, SearchOption.AllDirectories)
        .Select(path => new Folder(path));

    public IEnumerable<string> Files => Directory.GetFiles(this, FileWildcard, SearchOption.TopDirectoryOnly);

    public IEnumerable<string> AllFiles => Directory.GetFiles(this, FileWildcard, SearchOption.AllDirectories);

    public IFolder Up() => new Folder(new DirectoryInfo(this).Parent.FullName);

    public IFolder Down(string folderName) => new Folder(Path.Combine(this, folderName));

    public void Create() => Directory.CreateDirectory(this);

    public void Empty()
    {
        var directoryInfo = new DirectoryInfo(this);

        if (!directoryInfo.Exists)
        {
            return;
        }

        foreach (var file in AllFiles)
        {
            File.Delete(file);
        }

        foreach (var folder in Folders)
        {
            Directory.Delete(folder.ToString(), true);
        }
    }

    public void CopyTo(IFolder destination) => this.CopyTo(destination, file => true);

    public void CopyTo(IFolder destination, Func<string, bool> filter)
    {
        // Create directories
        foreach (var folder in this.AllFolders)
        {
            Directory.CreateDirectory(folder.ToString().Replace(this, destination.ToString()));
        }

        // Copy all the files & replaces any files with the same name
        foreach (var filePath in this.AllFiles.Where(filter))
        {
            File.Copy(filePath, filePath.Replace(this, destination.ToString()), true);
        }
    }

    public void Run(string exe, string args = "")
    {
        var defaultCurrentDirectory = Environment.CurrentDirectory;

        Environment.CurrentDirectory = this;
        try
        {
            var process = new Process { StartInfo = { FileName = exe, Arguments = args } };

            process.Start();
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                throw new InvalidOperationException(
                    $"{exe} process failed with exit code {process.ExitCode}.");
            }
        }
        finally
        {
            Environment.CurrentDirectory = defaultCurrentDirectory;
        }
    }
}

And finally, the Folders.cs implementation:

internal class Folders : IFolders
{
    private readonly Assembly _Assembly;

    public Folders(Assembly assembly = null)
    {
        this._Assembly = assembly ?? Assembly.GetExecutingAssembly();
    }

    public static IFolders Default => new Folders();

    public IFolder Bin => new Folder(this._Assembly);

    public IFolder App => this.Bin.Up();

    public IFolder Docs => this.App.Down("Docs");

    public IFolder Temp => this.App.Down("Temp");
}

Now that it's not static, you'll either have to create a new one with a particular assembly, or Folders.Default will have the current assembly as per original design.

\$\endgroup\$
  • \$\begingroup\$ Thank you Jesse. I have it as a package and found a little bit problematic to have wide interfaces in a reusable utility components. They have tendency to change, so client implementation will be broken. There is a good detailed discussion on this 4.3 Choosing Between Class and Interface, where they recommend to use abstract classes over interfaces for such distributable utility helpers. As for me personally - I would consider to mock an abstraction one level up if context allows - where file access service is implemented (Folder is a way too chatty). \$\endgroup\$ – Dmitry Nogin Jan 27 '16 at 4:51
  • \$\begingroup\$ I can't disagree with that assessment. I'd probably break down the monolithic interface into maybe three based on what I see as a division of concerns. \$\endgroup\$ – Jesse C. Slicer Jan 27 '16 at 4:55
  • \$\begingroup\$ On the link above they discuss it. Multiple interfaces make consumer to switch between them, which complicates life. This is why .NET utility classes are rarely backed up by specific interfaces (non existing IStream is specific, IDisposable is general) . Folder usually participates in a chatty scenarios and to be handy its API needs to be wide, so it is not so "interfacable" by nature. Well, it might be necessary because of the task definition, so we will take a burden of IFolder, but code will go to solution as a project (not as package). Interface is not always a silver bullet... :) \$\endgroup\$ – Dmitry Nogin Jan 27 '16 at 5:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.