1
\$\begingroup\$

This library is a layer of abstraction over file operations in C#. It aims to provide easy file IO syntax, and implicit error handling behaviors, to ensure robustness in scenarios including, but not limited to:

  • No file name supplied
  • Incorrect path supplied
  • File name is supplied without the path

But not only for error handling, it is written to make it more convenient for quick programs, like prototyping or demoing with C#. And to make file IO, and simple managing of state, easier.

This can be used for text files, and well as binary objects. And can run on multiple operating systems.

Here is the source code: Github

This is the demo project, that further illustrates the purpose, and syntax:

public class Demo
    {
        public static void Main()
        {
            //Simple operation with easy syntax. This can be done with any object.
            4096.ToFile();
            FileAbstraction.DisplayFile();

            //Library will search for this file, as the location is not given.
            8192.ToFile(@"..\Bas3 tw0 numb3r5.txt");
            FileAbstraction.DisplayFile("Bas3 tw0 numb3r5.txt");

            //A more complex search
            "This was found in my user folder".ToFile(@$"C:\Users\{Environment.UserName}\Downloads\\" + "myFile.txt");
            FileAbstraction.DisplayFile("myFile.txt");

            //An even more complex search, here the caller supplied an incorrect path. (Drive does not exist)
            "You found me, event though an incorrect path was given!"
                .ToFile("X:\\I hide here.txt");
            FileAbstraction.DisplayFile("I hide here.txt");

            //Also a search here. In this case, the file the caller wants, is on another drive (than what the app is running on).
            //NOTE: This will only work, if you computer has another drive.
            "You found me on another drive!".ToFile(@"E:\And I hide here.txt");
            FileAbstraction.DisplayFile("And I hide here.txt");

            //Serialization/De-Serialization of objects.
            var c = new Computer();
            c.ToFile();
            var savedC = FileAbstraction.ReadBinFile<Computer>();
            Console.WriteLine(savedC.Name);            
        }
    }
    internal class Computer
    {
        public Computer()
        {
            Name = Environment.MachineName;
            _description = RuntimeInformation.ProcessArchitecture;
        }
        public string Name { get; set; }
        private Architecture _description;

    }

This the "main" part of the library, in the sense that it contains the methods called by the client code:

/// <summary>
    /// For File => Object
    /// </summary>
    public static class FileAbstraction
    {
        public static T ReadBinFile<T>() where T : new()
        {
            var allFiles = Directory.GetFiles(Directory.GetCurrentDirectory());
            var latestFile = allFiles.Max(x => File.GetLastWriteTime(x));
            var path = new FilePath(allFiles.Where(x => (File.GetLastWriteTime(x) == latestFile)).Single());

            try
            {
                TextReader reader = new StreamReader(path.FileName);
                var fileContents = reader.ReadToEnd();
                var payload = JsonConvert.DeserializeObject<T>(fileContents);
                return payload is null ? new T() : payload;
            }
            catch
            {
                return new T();
            }                
        }
        public static string ReadFile()
        {
            var allFiles = Directory.GetFiles(Directory.GetCurrentDirectory());
            var latestFile = allFiles.Max(x => File.GetLastWriteTime(x));
            var fileName = allFiles.Where(x => (File.GetLastWriteTime(x) == latestFile)).Single();

            var file = new FileObject(fileName);
            return File.ReadAllText(file.FileName);
        }
        public static string ReadFile(string input)
        {
            //name or path
            if (Validation.IsDirectory(input))
            {
                var path = new FilePath(input);
                try
                {
                    return File.ReadAllText(path.FileName);
                }
                catch
                {
                    return path.SearchRead();
                }
            }
            else
            {
                var name = new FileName(input);
                var allFiles = Directory.GetFiles(Directory.GetCurrentDirectory());
                var match = allFiles.FirstOrDefault(x => x.Contains(name.FileName));

                return match is null ? name.SearchRead() : File.ReadAllText(match);
            }
        }
        public static void DisplayFile() => Console.WriteLine(ReadFile());
        public static void DisplayFile(string input) => Console.WriteLine(ReadFile(input));
    }
    /// <summary>
    /// For Object => File
    /// </summary>
    public static class FileExtensions
    {
        private static void WriteToFile<T>(T o, FilePath path)
        {
            if (o is not null)
            {
                if (o.GetType() == typeof(string))
                {
                    if (path.FileName.Contains(".txt"))
                    {
                        try
                        {
                            File.WriteAllText(path.FileName, o.ToString());
                        }
                        catch(DirectoryNotFoundException)
                        {
                            File.WriteAllText(Directory.GetCurrentDirectory() + Validation.SlashChar + Path.GetFileName(path.FileName), o.ToString());
                        }
                    }
                    else
                    {
                        File.WriteAllText(path.FileName + ".txt", o.ToString());
                    }
                }
                else
                {
                    File.WriteAllBytes(path.FileName, o.ObjectToByteArray());
                }
            }
        }
        public static void ToFile<T>(this T o)
        {
            FilePath filePath = new FilePath(AppDomain.CurrentDomain.BaseDirectory + @$"{Validation.SlashChar}{Environment.UserName}");
            WriteToFile(o, filePath);

        }
        public static void ToFile<T>(this T o, string input)
        {
            if (Validation.IsDirectory(input))
            {
                var path = new FilePath(input);
                WriteToFile(o, path);
            }
            else
            {
                var fileName = new FileName(input);
                var path = new FilePath(AppDomain.CurrentDomain.BaseDirectory + @$"{fileName.FileName}");
                WriteToFile(o, path);
            }
        }
    }

This Data class, contains logic for special data types, and operations related to file IO:

abstract internal class DirectoryItem
{
    public string FileName => _fileName is null ? "" : _fileName;
    protected string? _fileName;
    protected string CorrectedFileName(string fileName)
    {
        if (Validation.IsWindows() && (fileName.IndexOfAny(Validation.InvalidWindowsChars) != -1))
        {
            foreach (char c in fileName)
            {
                if (Validation.InvalidWindowsChars.Contains(c)) { fileName.Replace(c.ToString(), ""); }
            }
        }
        return fileName;
    }
    protected string TrimmedFileName(string fileName)
    {
        return fileName.Length > Validation.MaxFileNameLength ? fileName.Substring((fileName.Length - 1) - Validation.MaxFileNameLength, Validation.MaxFileNameLength) : fileName;  //Trim file if too long, avoiding file system error
    }
}
internal class FileName : DirectoryItem
{
    public FileName(string name)
    {
        name = Validation.IsDirectory(name) ? Path.GetFileName(name) : name;
        name = CorrectedFileName(name);
        name = TrimmedFileName(name);
        _fileName = name;
    }
}
internal class FilePath : DirectoryItem
{
    public FilePath(string filePath)
    {
        filePath = CorrectedFileName(filePath);

        if(filePath.Length > Validation.MaxDirectoryLength)
        {
            FileName name = new FileName(Path.GetFileName(filePath).Substring(0,Validation.MaxDirectoryLength));
            var dir = Path.GetDirectoryName(filePath);
            _fileName = dir + Validation.SlashChar + name.FileName;
        }
        else
        {
            _fileName = filePath;
        }            
    }
}
internal class FileObject : DirectoryItem
{
    private string _fileExtensionText;
    private FileType _type;
    public FileObject(string fileName)
    {
        _fileName = fileName;
        if (fileName.Length > 3)
        {
            _type = fileName.Substring(fileName.Length - 4, 4) == ".txt" ? FileType.text : FileType.binary;
        }
        else
        {
            _type = FileType.binary;
        }
        _fileExtensionText = fileName.Contains('.') ? fileName.Substring(fileName.LastIndexOf('.')) : "";
    }
    public FileType Type => _type;
}
enum FileType
{
    text,
    binary
}
internal static class DataOperationExensions
{
    public static byte[] ObjectToByteArray<T>(this T o)
    {
        if (o == null)
            return new byte[] { };

        return Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(o, GetJsonSerializerOptions()));
    }        
    private static JsonSerializerOptions GetJsonSerializerOptions()
    {
        return new JsonSerializerOptions()
        {
            PropertyNamingPolicy = null,
            WriteIndented = true,
            AllowTrailingCommas = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }
    internal static string SearchRead(this DirectoryItem directoryItem)
    {
        var fileName = Validation.IsDirectory(directoryItem.FileName)
            ? directoryItem.FileName.Substring(directoryItem.FileName.LastIndexOf(Validation.SlashChar) + 1, (directoryItem.FileName.Length - 1) - (directoryItem.FileName.LastIndexOf(Validation.SlashChar) + 1))
            : directoryItem.FileName;

        var startDir = Directory.GetCurrentDirectory();

        //Search deeper
        var subDirectories = Directory.GetDirectories(startDir, "*", SearchOption.AllDirectories);

        foreach (var subDirectory in subDirectories)
        {
            foreach (var filePath in Directory.GetFiles(subDirectory))
            {
                var name = new FileName(filePath);
                if (name.FileName == fileName) return File.ReadAllText(filePath);
            }
        }

        var drives = DriveInfo.GetDrives();
        var thisDrive = drives.Where(x => x.Name == startDir.Substring(0, 3)).Single();

        //Search back
        var currentDir = startDir;
        do
        {
            currentDir = Path.GetFullPath(Path.Combine(currentDir, ".."));
            foreach (var filePath in Directory.GetFiles(currentDir))
            {
                var name = new FileName(filePath);
                if (name.FileName == fileName) return File.ReadAllText(filePath);
            }

        } while (currentDir != thisDrive.Name);


        //Search all drives
        foreach (var drive in drives)
        {
            if (drive.IsReady)
            {
                var resultOnDrive = WalkDirectoryTree(new DirectoryInfo(drive.Name), fileName);
                if (resultOnDrive.Length > 0)
                    return resultOnDrive;
            }
        }
        return "";  //File not found
    }
    private static string WalkDirectoryTree(System.IO.DirectoryInfo root, string fileName)
    {
        System.IO.FileInfo[] files = null;
        System.IO.DirectoryInfo[] subDirs = null;

        // First, process all the files directly under this folder
        try
        {
            files = root.GetFiles("*.*");
        }
        catch (UnauthorizedAccessException)
        {
        }

        catch (System.IO.DirectoryNotFoundException)
        {
        }

        if (files != null)
        {
            foreach (System.IO.FileInfo fi in files)
            {
                if (fi.Name == fileName) return File.ReadAllText(fi.FullName);
            }

            subDirs = root.GetDirectories();
            foreach (System.IO.DirectoryInfo dirInfo in subDirs)
            {
                // Resursive call for each subdirectory.
                if (!(dirInfo.Name == "Windows" || dirInfo.Name == "Program Files(x86)" || dirInfo.Name == "Program Files"))    //Skip system folders, they are large and the user file is probably not here.
                {
                    WalkDirectoryTree(dirInfo, fileName);
                }
            }
        }
        return "";
    }
}

This validation class, is primarily used by the Data class:

internal class Validation
{
    internal static bool IsDirectory(string s) => s.Contains(SlashChar);
    internal static int MaxFileNameLength => IsWindows() ? ((IsLongPathsEnabled()) ? 32767 : 255) : 255;
    internal static int MaxDirectoryLength => IsLinux() ? 4096 : 260;
    internal static char SlashChar => IsWindows() ? '\\' : '/';
    internal static char[] InvalidWindowsChars => new char[] { '<', '>', ':', '"', '/', '|', '?', '*' };
    internal static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
    internal static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

    internal static bool IsLongPathsEnabled()
    {
        var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\FileSystem");
        if (key is null)
        {
            return false;
        }

        var regVal = key.GetValue("LongPathsEnabled");
        if (regVal is null)
        {
            return false;
        }

        return (int)regVal > 0;
    }
}
\$\endgroup\$
2
  • 2
    \$\begingroup\$ To be honest, I'm not seeing anything that would fall under the programming definition of abstraction. These are, by and large, convenience extensions, (thin) shortcuts on top of existing functionality. \$\endgroup\$ Jul 31, 2022 at 21:19
  • \$\begingroup\$ @JesseC.Slicer accepting that it is convenience extentions and not abstractions, do you have any comments on the code itself, in terms of how I can improve it? \$\endgroup\$
    – Divan
    Aug 1, 2022 at 6:54

1 Answer 1

5
\$\begingroup\$

Okay, here are some quick random thoughts:

  • in the places where there is stuff.Where(something).Single() (or .First(), etc.), just remove the .Where(something) and put something in the parameter of the Single(): stuff.Single(something).

  • there are a couple places where you have a string as @$"stuff{other_stuff}" where there's no need for the @ because the code doesn't escape any characters or embed literal cr/lfs. Just remove it. The $ is necessary though as they are templated strings.

  • if o.GetType() == typeof(string) - simply replace with if o is string. In fact, then the previous line if o is not null is not needed at all.

  • The using statement: a number of types, specifically I/O types, implement the IDisposable interface and therefore must be wrapped in a using construct. For example:

TextReader reader = new StreamReader(path.FileName);
var fileContents = reader.ReadToEnd();
var payload = JsonConvert.DeserializeObject<T>(fileContents);
return payload is null ? new T() : payload;

should be

string fileContents;
using (TextReader reader = new StreamReader(path.FileName))
{
    fileContents = reader.ReadToEnd();
}
var payload = JsonConvert.DeserializeObject<T>(fileContents);
return payload is null ? new T() : payload;
  • Minor, but improves readability: when you have a construct such as something.Max(x => Method(x)), it can be simplified as a "method group" to something.Max(Method).

  • in FileObject, get rid of the class member variable _type and make the property Type an "automatic property": public FileType Type { get; } and just assign directly to the property in the constructor.

  • the enum FileType should follow standard naming conventions and be PascalCased: Text, Binary. Further, you may want to prepend Unknown just to be able to code against that scenario if your code receives a 0 (zero) accidentally.

  • Always add braces {} around the body of an if or else, even if it is one single statement. This future-proofs a whole class of mistakes when needing to add to that body.

  • In class Computer, I don't see a need for the Name property to have a setter since the constructor fills it out. In fact, make both members automatic read-only properties:

public string Name { get; }
public Architecture Description { get; }
  • I know the Main program is just an example, but consider Path.Combine() rather than hard-coding backslashes like C:\\users\\ etc. Similarly, in class Validation, the SlashChar property is already implemented in .NET as Path.DirectorySeparatorChar.

  • This may change in the future, but I don't believe the operating system will change while the program is running, so the two methods

internal static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
internal static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

could be rewritten as properties

internal static bool IsWindows { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
internal static bool IsLinux { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

and forego calling the runtime information methods repeatedly.

  • Empty catch blocks are generally a cause of heartache. I like to add something in there, even if it's just for my own debugging purposes:
catch (SomeKindOfException ex)
{
    System.Diagnostics.Debug.WriteLine($"Swallowed exception: {ex}");
}
  • Method WalkDirectoryTree is very Windows-specific as it doesn't skip Linux "system" folders and will erroneously skip Linux folders with those names. .NET helps you with this, and I'd rewrite part of that method slightly to remove the hard-coding:
var specialFolders = Enum.GetValues(typeof(Environment.SpecialFolder)).Cast<Environment.SpecialFolder>().Select(Environment.GetFolderPath).ToList();
foreach (System.IO.DirectoryInfo dirInfo in subDirs)
{
    // Recursive call for each subdirectory.
    if (!specialFolders.Contains(dirInfo.FullName))
    {
        WalkDirectoryTree(dirInfo, fileName);
    }
}
  • new byte[] { } can be replaced with Array.Empty<byte>(); which won't create a new object that will get GC'd later.

  • var x = something is null ? blah : something (or something == null) can be replaced with "null coalescing operator": var x = something ?? blah.

  • This loop, as written, in CorrectedFileName does nothing:

foreach (char c in fileName)
{
    if (Validation.InvalidWindowsChars.Contains(c))
    {
        fileName.Replace(c.ToString(), "");
    }
}

because strings are immutable and String.Replace() returns a string. Flip the loop around:

foreach (char c in Validation.InvalidWindowsChars)
{
    if (fileName.Contains(c))
    {
         fileName = fileName.Replace(c.ToString(), string.Empty);
    }
}

better yet, use LINQ:

foreach (var c in Validation.InvalidWindowsChars.Where(c => fileName.Contains(c)))
{
    fileName = fileName.Replace(c.ToString(), string.Empty);
}

There's probably a bit more if I dig deeper, but this seems like a good start.

\$\endgroup\$
2
  • \$\begingroup\$ These seem like solid improvements. I will implement them. \$\endgroup\$
    – Divan
    Aug 1, 2022 at 18:06
  • 1
    \$\begingroup\$ I just completed the refactoring, as you suggested above. I thank you for this, I have learned from you. And any more suggestions would of course, be most welcome. \$\endgroup\$
    – Divan
    Aug 2, 2022 at 17:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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