Multiple file access abstractions











up vote
5
down vote

favorite
1












When using dependency injection for nearly everything it's good to have some file access abstraction. I find the idea of ASP.NET Core FileProvider nice but not sufficient for my needs so inspired by that I decided to create my own with some more functionality.





Interfaces



I have two interfaces that are called just like theirs but they have different members and also other names.



The first interface represents a single file or a directory.



[PublicAPI]
public interface IFileInfo : IEquatable<IFileInfo>, IEquatable<string>
{
[NotNull]
string Path { get; }

[NotNull]
string Name { get; }

bool Exists { get; }

long Length { get; }

DateTime ModifiedOn { get; }

bool IsDirectory { get; }

[NotNull]
Stream CreateReadStream();
}


The other interface allows me to perform four of the basic file/directory operations:



[PublicAPI]
public interface IFileProvider
{
[NotNull]
IFileInfo GetFileInfo([NotNull] string path);

[NotNull]
IFileInfo CreateDirectory([NotNull] string path);

[NotNull]
IFileInfo DeleteDirectory([NotNull] string path, bool recursive);

[NotNull]
Task<IFileInfo> CreateFileAsync([NotNull] string path, [NotNull] Stream data);

[NotNull]
IFileInfo DeleteFile([NotNull] string path);
}




On top of them I've build three providers:





  • PhysicalFileProvider and PhysicalFileInfo - used for operation on the physical drive


  • EmbeddedFileProvider and EmbeddedFileInfo - used for reading of embedded resources (primarily for testing); internally, it automatically adds the root namespace of the specified assembly to the path


  • InMemoryFileProvider and InMemoryFileInfo - used for testing or runtime data


There is no GetDirectoryContents API because this is what I have the DirectoryTree for.



All providers use the same path schema, this is, with the backslash . This is also why the EmbeddedFileProvider does some additional converting between the usual patch and the resource path which is separated by dots .





Implementations



So here they are, the three pairs, in the same order as the above list:



[PublicAPI]
public class PhysicalFileProvider : IFileProvider
{
public IFileInfo GetFileInfo(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));

return new PhysicalFileInfo(path);
}

public IFileInfo CreateDirectory(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));

if (Directory.Exists(path))
{
return new PhysicalFileInfo(path);
}

try
{
var newDirectory = Directory.CreateDirectory(path);
return new PhysicalFileInfo(newDirectory.FullName);
}
catch (Exception ex)
{
throw new CreateDirectoryException(path, ex);
}
}

public async Task<IFileInfo> CreateFileAsync(string path, Stream data)
{
try
{
using (var fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
{
await data.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
return new PhysicalFileInfo(path);
}
catch (Exception ex)
{
throw new CreateFileException(path, ex);
}
}

public IFileInfo DeleteFile(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));

try
{
File.Delete(path);
return new PhysicalFileInfo(path);
}
catch (Exception ex)
{
throw new DeleteFileException(path, ex);
}
}

public IFileInfo DeleteDirectory(string path, bool recursive)
{
try
{
Directory.Delete(path, recursive);
return new PhysicalFileInfo(path);
}
catch (Exception ex)
{
throw new DeleteDirectoryException(path, ex);
}
}
}

[PublicAPI]
internal class PhysicalFileInfo : IFileInfo
{
public PhysicalFileInfo([NotNull] string path) => Path = path ?? throw new ArgumentNullException(nameof(path));

#region IFileInfo

public string Path { get; }

public string Name => System.IO.Path.GetFileName(Path);

public bool Exists => File.Exists(Path) || Directory.Exists(Path);

public long Length => Exists && !IsDirectory ? new FileInfo(Path).Length : -1;

public DateTime ModifiedOn => !string.IsNullOrEmpty(Path) ? File.GetLastWriteTime(Path) : default;

public bool IsDirectory => Directory.Exists(Path);

public Stream CreateReadStream()
{
return
IsDirectory
? throw new InvalidOperationException($"Cannot open '{Path}' for reading because it's a directory.")
: Exists
? File.OpenRead(Path)
: throw new InvalidOperationException("Cannot open '{Path}' for reading because the file does not exist.");
}

#endregion

#region IEquatable<IFileInfo>

public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

#endregion
}




public class EmbeddedFileProvider : IFileProvider
{
private readonly Assembly _assembly;

public EmbeddedFileProvider([NotNull] Assembly assembly)
{
_assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
BasePath = _assembly.GetName().Name.Replace('.', '\');
}

public string BasePath { get; }

public IFileInfo GetFileInfo(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));

// Embedded resouce names are separated by '.' so replace the windows separator.
var fullName = Path.Combine(BasePath, path).Replace('\', '.');

// Embedded resource names are case sensitive so find the actual name of the resource.
var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);

return new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream);
}

// Convert path back to windows format but the last '.' - this is the file extension.
private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", "\");

public IFileInfo CreateDirectory(string path)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory creation.");
}

public IFileInfo DeleteDirectory(string path, bool recursive)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory deletion.");
}

public Task<IFileInfo> CreateFileAsync(string path, Stream data)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file creation.");
}

public IFileInfo DeleteFile(string path)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file deletion.");
}
}

internal class EmbeddedFileInfo : IFileInfo
{
private readonly Func<Stream> _getManifestResourceStream;

public EmbeddedFileInfo(string path, Func<Stream> getManifestResourceStream)
{
_getManifestResourceStream = getManifestResourceStream;
Path = path;
}

public string Path { get; }

public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

public bool Exists => !(_getManifestResourceStream is null);

public long Length => _getManifestResourceStream()?.Length ?? -1;

public DateTime ModifiedOn { get; }

public bool IsDirectory => false;

// No protection necessary because there are no embedded directories.
public Stream CreateReadStream() => _getManifestResourceStream();

#region IEquatable<IFileInfo>

public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

#endregion
}




public class InMemoryFileProvider : Dictionary<string, byte>, IFileProvider
{
private readonly ISet<IFileInfo> _files = new HashSet<IFileInfo>();

#region IFileProvider

public IFileInfo GetFileInfo(string path)
{
var file = _files.SingleOrDefault(f => FileInfoEqualityComparer.Default.Equals(f.Path, path));
return file ?? new InMemoryFileInfo(path, default(byte));
}

public IFileInfo CreateDirectory(string path)
{
path = path.TrimEnd('\');
var newDirectory = new InMemoryFileInfo(path, _files.Where(f => f.Path.StartsWith(path)));
_files.Add(newDirectory);
return newDirectory;
}

public IFileInfo DeleteDirectory(string path, bool recursive)
{
return DeleteFile(path);

}
public Task<IFileInfo> CreateFileAsync(string path, Stream data)
{
var file = new InMemoryFileInfo(path, GetByteArray(data));
_files.Remove(file);
_files.Add(file);
return Task.FromResult<IFileInfo>(file);

byte GetByteArray(Stream stream)
{
using (var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}

public IFileInfo DeleteFile(string path)
{
var fileToDelete = new InMemoryFileInfo(path, default(byte));
_files.Remove(fileToDelete);
return fileToDelete;
}

#endregion
}

internal class InMemoryFileInfo : IFileInfo
{
[CanBeNull]
private readonly byte _data;

[CanBeNull]
private readonly IEnumerable<IFileInfo> _files;

private InMemoryFileInfo([NotNull] string path)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
ModifiedOn = DateTime.UtcNow;
}

public InMemoryFileInfo([NotNull] string path, byte data)
: this(path)
{
_data = data;
Exists = !(data is null);
IsDirectory = false;
}

public InMemoryFileInfo([NotNull] string path, [NotNull] IEnumerable<IFileInfo> files)
: this(path)
{
_files = files ?? throw new ArgumentNullException(nameof(files));
Exists = true;
IsDirectory = true;
}

#region IFileInfo

public bool Exists { get; }

public long Length => IsDirectory ? throw new InvalidOperationException("Directories have no length.") : _data?.Length ?? -1;

public string Path { get; }

public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

public DateTime ModifiedOn { get; }

public bool IsDirectory { get; }

public Stream CreateReadStream()
{
return
IsDirectory
? throw new InvalidOperationException("Cannot create read-stream for a directory.")
: Exists
// ReSharper disable once AssignNullToNotNullAttribute - this is never null because it's protected by Exists.
? new MemoryStream(_data)
: throw new InvalidOperationException("Cannot create a read-stream for a file that does not exist.");
}

#endregion

#region IEquatable<IFileInfo>

public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

#endregion
}




Decorator for less typing



There is one more file provider. I use this to save some typing of paths. The RelativeFileProvider adds its path in front of the other path if there is some root path that doesn't change.



public class RelativeFileProvider : IFileProvider
{
private readonly IFileProvider _fileProvider;

private readonly string _basePath;

public RelativeFileProvider([NotNull] IFileProvider fileProvider, [NotNull] string basePath)
{
_fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
_basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
}

public IFileInfo GetFileInfo(string path) => _fileProvider.GetFileInfo(CreateFullPath(path));

public IFileInfo CreateDirectory(string path) => _fileProvider.CreateDirectory(CreateFullPath(path));

public IFileInfo DeleteDirectory(string path, bool recursive) => _fileProvider.DeleteDirectory(CreateFullPath(path), recursive);

public Task<IFileInfo> CreateFileAsync(string path, Stream data) => _fileProvider.CreateFileAsync(CreateFullPath(path), data);

public IFileInfo DeleteFile(string path) => _fileProvider.DeleteFile(CreateFullPath(path));

private string CreateFullPath(string path) => Path.Combine(_basePath, path ?? throw new ArgumentNullException(nameof(path)));
}




Exceptions



The providers don't throw pure .NET exception because they aren't usually helpful. I wrap them in my own types:



public class CreateDirectoryException : Exception
{
public CreateDirectoryException(string path, Exception innerException)
: base($"Could not create directory: {path}", innerException)
{ }
}

public class CreateFileException : Exception
{
public CreateFileException(string path, Exception innerException)
: base($"Could not create file: {path}", innerException)
{ }
}

public class DeleteDirectoryException : Exception
{
public DeleteDirectoryException(string path, Exception innerException)
: base($"Could not delete directory: {path}", innerException)
{ }
}

public class DeleteFileException : Exception
{
public DeleteFileException(string path, Exception innerException)
: base($"Could not delete file: {path}", innerException)
{ }
}




Simple file search



There is one more provider that allows me to probe multiple providers. It supports only reading:



public class CompositeFileProvider : IFileProvider
{
private readonly IEnumerable<IFileProvider> _fileProviders;

public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders)
{
_fileProviders = fileProviders;
}

public IFileInfo GetFileInfo(string path)
{
foreach (var fileProvider in _fileProviders)
{
var fileInfo = fileProvider.GetFileInfo(path);
if (fileInfo.Exists)
{
return fileInfo;
}
}

return new InMemoryFileInfo(path, new byte[0]);
}

public IFileInfo CreateDirectory(string path)
{
throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory creation.");
}

public IFileInfo DeleteDirectory(string path, bool recursive)
{
throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory deletion.");
}

public Task<IFileInfo> CreateFileAsync(string path, Stream data)
{
throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file creation.");
}

public IFileInfo DeleteFile(string path)
{
throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file deletion.");
}
}




Comparing files



The comparer for the IFileInfo is very straightforward and compares the Path property:



public class FileInfoEqualityComparer : IEqualityComparer<IFileInfo>, IEqualityComparer<string>
{
private static readonly IEqualityComparer PathComparer = StringComparer.OrdinalIgnoreCase;

[NotNull]
public static FileInfoEqualityComparer Default { get; } = new FileInfoEqualityComparer();

public bool Equals(IFileInfo x, IFileInfo y) => Equals(x?.Path, y?.Path);

public int GetHashCode(IFileInfo obj) => GetHashCode(obj.Path);

public bool Equals(string x, string y) => PathComparer.Equals(x, y);

public int GetHashCode(string obj) => PathComparer.GetHashCode(obj);
}




Example



As an example I use one of my tests that checks whether the relative and embedded files providers do their job correctly.



[TestClass]
public class RelativeFileProviderTest
{
[TestMethod]
public void GetFileInfo_DoesNotGetNonExistingEmbeddedFile()
{
var fileProvider =
new RelativeFileProvider(
new EmbeddedFileProvider(typeof(RelativeFileProviderTest).Assembly),
@"relativepath");

var file = fileProvider.GetFileInfo(@"file.ext");

Assert.IsFalse(file.Exists);
Assert.IsTrue(SoftString.Comparer.Equals(@"ReusableTestsrelativepathfile.ext", file.Path));
}
}




Questions



Besides of the default question about can this be improved in anyway I have more:




  • should I be concerned about thread-safty here? I didn't use any locks but adding them isn't a big deal. Should I? Where would you add them? I guess creating files and directories could be good candidates, right?

  • should the EmbeddedFileProvider use the RelativeFileProvider to add the assembly namespace to the path or should I leave it as is?










share|improve this question


























    up vote
    5
    down vote

    favorite
    1












    When using dependency injection for nearly everything it's good to have some file access abstraction. I find the idea of ASP.NET Core FileProvider nice but not sufficient for my needs so inspired by that I decided to create my own with some more functionality.





    Interfaces



    I have two interfaces that are called just like theirs but they have different members and also other names.



    The first interface represents a single file or a directory.



    [PublicAPI]
    public interface IFileInfo : IEquatable<IFileInfo>, IEquatable<string>
    {
    [NotNull]
    string Path { get; }

    [NotNull]
    string Name { get; }

    bool Exists { get; }

    long Length { get; }

    DateTime ModifiedOn { get; }

    bool IsDirectory { get; }

    [NotNull]
    Stream CreateReadStream();
    }


    The other interface allows me to perform four of the basic file/directory operations:



    [PublicAPI]
    public interface IFileProvider
    {
    [NotNull]
    IFileInfo GetFileInfo([NotNull] string path);

    [NotNull]
    IFileInfo CreateDirectory([NotNull] string path);

    [NotNull]
    IFileInfo DeleteDirectory([NotNull] string path, bool recursive);

    [NotNull]
    Task<IFileInfo> CreateFileAsync([NotNull] string path, [NotNull] Stream data);

    [NotNull]
    IFileInfo DeleteFile([NotNull] string path);
    }




    On top of them I've build three providers:





    • PhysicalFileProvider and PhysicalFileInfo - used for operation on the physical drive


    • EmbeddedFileProvider and EmbeddedFileInfo - used for reading of embedded resources (primarily for testing); internally, it automatically adds the root namespace of the specified assembly to the path


    • InMemoryFileProvider and InMemoryFileInfo - used for testing or runtime data


    There is no GetDirectoryContents API because this is what I have the DirectoryTree for.



    All providers use the same path schema, this is, with the backslash . This is also why the EmbeddedFileProvider does some additional converting between the usual patch and the resource path which is separated by dots .





    Implementations



    So here they are, the three pairs, in the same order as the above list:



    [PublicAPI]
    public class PhysicalFileProvider : IFileProvider
    {
    public IFileInfo GetFileInfo(string path)
    {
    if (path == null) throw new ArgumentNullException(nameof(path));

    return new PhysicalFileInfo(path);
    }

    public IFileInfo CreateDirectory(string path)
    {
    if (path == null) throw new ArgumentNullException(nameof(path));

    if (Directory.Exists(path))
    {
    return new PhysicalFileInfo(path);
    }

    try
    {
    var newDirectory = Directory.CreateDirectory(path);
    return new PhysicalFileInfo(newDirectory.FullName);
    }
    catch (Exception ex)
    {
    throw new CreateDirectoryException(path, ex);
    }
    }

    public async Task<IFileInfo> CreateFileAsync(string path, Stream data)
    {
    try
    {
    using (var fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
    {
    await data.CopyToAsync(fileStream);
    await fileStream.FlushAsync();
    }
    return new PhysicalFileInfo(path);
    }
    catch (Exception ex)
    {
    throw new CreateFileException(path, ex);
    }
    }

    public IFileInfo DeleteFile(string path)
    {
    if (path == null) throw new ArgumentNullException(nameof(path));

    try
    {
    File.Delete(path);
    return new PhysicalFileInfo(path);
    }
    catch (Exception ex)
    {
    throw new DeleteFileException(path, ex);
    }
    }

    public IFileInfo DeleteDirectory(string path, bool recursive)
    {
    try
    {
    Directory.Delete(path, recursive);
    return new PhysicalFileInfo(path);
    }
    catch (Exception ex)
    {
    throw new DeleteDirectoryException(path, ex);
    }
    }
    }

    [PublicAPI]
    internal class PhysicalFileInfo : IFileInfo
    {
    public PhysicalFileInfo([NotNull] string path) => Path = path ?? throw new ArgumentNullException(nameof(path));

    #region IFileInfo

    public string Path { get; }

    public string Name => System.IO.Path.GetFileName(Path);

    public bool Exists => File.Exists(Path) || Directory.Exists(Path);

    public long Length => Exists && !IsDirectory ? new FileInfo(Path).Length : -1;

    public DateTime ModifiedOn => !string.IsNullOrEmpty(Path) ? File.GetLastWriteTime(Path) : default;

    public bool IsDirectory => Directory.Exists(Path);

    public Stream CreateReadStream()
    {
    return
    IsDirectory
    ? throw new InvalidOperationException($"Cannot open '{Path}' for reading because it's a directory.")
    : Exists
    ? File.OpenRead(Path)
    : throw new InvalidOperationException("Cannot open '{Path}' for reading because the file does not exist.");
    }

    #endregion

    #region IEquatable<IFileInfo>

    public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

    public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

    public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

    public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

    #endregion
    }




    public class EmbeddedFileProvider : IFileProvider
    {
    private readonly Assembly _assembly;

    public EmbeddedFileProvider([NotNull] Assembly assembly)
    {
    _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
    BasePath = _assembly.GetName().Name.Replace('.', '\');
    }

    public string BasePath { get; }

    public IFileInfo GetFileInfo(string path)
    {
    if (path == null) throw new ArgumentNullException(nameof(path));

    // Embedded resouce names are separated by '.' so replace the windows separator.
    var fullName = Path.Combine(BasePath, path).Replace('\', '.');

    // Embedded resource names are case sensitive so find the actual name of the resource.
    var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
    var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);

    return new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream);
    }

    // Convert path back to windows format but the last '.' - this is the file extension.
    private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", "\");

    public IFileInfo CreateDirectory(string path)
    {
    throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory creation.");
    }

    public IFileInfo DeleteDirectory(string path, bool recursive)
    {
    throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory deletion.");
    }

    public Task<IFileInfo> CreateFileAsync(string path, Stream data)
    {
    throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file creation.");
    }

    public IFileInfo DeleteFile(string path)
    {
    throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file deletion.");
    }
    }

    internal class EmbeddedFileInfo : IFileInfo
    {
    private readonly Func<Stream> _getManifestResourceStream;

    public EmbeddedFileInfo(string path, Func<Stream> getManifestResourceStream)
    {
    _getManifestResourceStream = getManifestResourceStream;
    Path = path;
    }

    public string Path { get; }

    public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

    public bool Exists => !(_getManifestResourceStream is null);

    public long Length => _getManifestResourceStream()?.Length ?? -1;

    public DateTime ModifiedOn { get; }

    public bool IsDirectory => false;

    // No protection necessary because there are no embedded directories.
    public Stream CreateReadStream() => _getManifestResourceStream();

    #region IEquatable<IFileInfo>

    public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

    public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

    public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

    public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

    #endregion
    }




    public class InMemoryFileProvider : Dictionary<string, byte>, IFileProvider
    {
    private readonly ISet<IFileInfo> _files = new HashSet<IFileInfo>();

    #region IFileProvider

    public IFileInfo GetFileInfo(string path)
    {
    var file = _files.SingleOrDefault(f => FileInfoEqualityComparer.Default.Equals(f.Path, path));
    return file ?? new InMemoryFileInfo(path, default(byte));
    }

    public IFileInfo CreateDirectory(string path)
    {
    path = path.TrimEnd('\');
    var newDirectory = new InMemoryFileInfo(path, _files.Where(f => f.Path.StartsWith(path)));
    _files.Add(newDirectory);
    return newDirectory;
    }

    public IFileInfo DeleteDirectory(string path, bool recursive)
    {
    return DeleteFile(path);

    }
    public Task<IFileInfo> CreateFileAsync(string path, Stream data)
    {
    var file = new InMemoryFileInfo(path, GetByteArray(data));
    _files.Remove(file);
    _files.Add(file);
    return Task.FromResult<IFileInfo>(file);

    byte GetByteArray(Stream stream)
    {
    using (var memoryStream = new MemoryStream())
    {
    stream.CopyTo(memoryStream);
    return memoryStream.ToArray();
    }
    }
    }

    public IFileInfo DeleteFile(string path)
    {
    var fileToDelete = new InMemoryFileInfo(path, default(byte));
    _files.Remove(fileToDelete);
    return fileToDelete;
    }

    #endregion
    }

    internal class InMemoryFileInfo : IFileInfo
    {
    [CanBeNull]
    private readonly byte _data;

    [CanBeNull]
    private readonly IEnumerable<IFileInfo> _files;

    private InMemoryFileInfo([NotNull] string path)
    {
    Path = path ?? throw new ArgumentNullException(nameof(path));
    ModifiedOn = DateTime.UtcNow;
    }

    public InMemoryFileInfo([NotNull] string path, byte data)
    : this(path)
    {
    _data = data;
    Exists = !(data is null);
    IsDirectory = false;
    }

    public InMemoryFileInfo([NotNull] string path, [NotNull] IEnumerable<IFileInfo> files)
    : this(path)
    {
    _files = files ?? throw new ArgumentNullException(nameof(files));
    Exists = true;
    IsDirectory = true;
    }

    #region IFileInfo

    public bool Exists { get; }

    public long Length => IsDirectory ? throw new InvalidOperationException("Directories have no length.") : _data?.Length ?? -1;

    public string Path { get; }

    public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

    public DateTime ModifiedOn { get; }

    public bool IsDirectory { get; }

    public Stream CreateReadStream()
    {
    return
    IsDirectory
    ? throw new InvalidOperationException("Cannot create read-stream for a directory.")
    : Exists
    // ReSharper disable once AssignNullToNotNullAttribute - this is never null because it's protected by Exists.
    ? new MemoryStream(_data)
    : throw new InvalidOperationException("Cannot create a read-stream for a file that does not exist.");
    }

    #endregion

    #region IEquatable<IFileInfo>

    public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

    public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

    public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

    public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

    #endregion
    }




    Decorator for less typing



    There is one more file provider. I use this to save some typing of paths. The RelativeFileProvider adds its path in front of the other path if there is some root path that doesn't change.



    public class RelativeFileProvider : IFileProvider
    {
    private readonly IFileProvider _fileProvider;

    private readonly string _basePath;

    public RelativeFileProvider([NotNull] IFileProvider fileProvider, [NotNull] string basePath)
    {
    _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
    _basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
    }

    public IFileInfo GetFileInfo(string path) => _fileProvider.GetFileInfo(CreateFullPath(path));

    public IFileInfo CreateDirectory(string path) => _fileProvider.CreateDirectory(CreateFullPath(path));

    public IFileInfo DeleteDirectory(string path, bool recursive) => _fileProvider.DeleteDirectory(CreateFullPath(path), recursive);

    public Task<IFileInfo> CreateFileAsync(string path, Stream data) => _fileProvider.CreateFileAsync(CreateFullPath(path), data);

    public IFileInfo DeleteFile(string path) => _fileProvider.DeleteFile(CreateFullPath(path));

    private string CreateFullPath(string path) => Path.Combine(_basePath, path ?? throw new ArgumentNullException(nameof(path)));
    }




    Exceptions



    The providers don't throw pure .NET exception because they aren't usually helpful. I wrap them in my own types:



    public class CreateDirectoryException : Exception
    {
    public CreateDirectoryException(string path, Exception innerException)
    : base($"Could not create directory: {path}", innerException)
    { }
    }

    public class CreateFileException : Exception
    {
    public CreateFileException(string path, Exception innerException)
    : base($"Could not create file: {path}", innerException)
    { }
    }

    public class DeleteDirectoryException : Exception
    {
    public DeleteDirectoryException(string path, Exception innerException)
    : base($"Could not delete directory: {path}", innerException)
    { }
    }

    public class DeleteFileException : Exception
    {
    public DeleteFileException(string path, Exception innerException)
    : base($"Could not delete file: {path}", innerException)
    { }
    }




    Simple file search



    There is one more provider that allows me to probe multiple providers. It supports only reading:



    public class CompositeFileProvider : IFileProvider
    {
    private readonly IEnumerable<IFileProvider> _fileProviders;

    public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders)
    {
    _fileProviders = fileProviders;
    }

    public IFileInfo GetFileInfo(string path)
    {
    foreach (var fileProvider in _fileProviders)
    {
    var fileInfo = fileProvider.GetFileInfo(path);
    if (fileInfo.Exists)
    {
    return fileInfo;
    }
    }

    return new InMemoryFileInfo(path, new byte[0]);
    }

    public IFileInfo CreateDirectory(string path)
    {
    throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory creation.");
    }

    public IFileInfo DeleteDirectory(string path, bool recursive)
    {
    throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory deletion.");
    }

    public Task<IFileInfo> CreateFileAsync(string path, Stream data)
    {
    throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file creation.");
    }

    public IFileInfo DeleteFile(string path)
    {
    throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file deletion.");
    }
    }




    Comparing files



    The comparer for the IFileInfo is very straightforward and compares the Path property:



    public class FileInfoEqualityComparer : IEqualityComparer<IFileInfo>, IEqualityComparer<string>
    {
    private static readonly IEqualityComparer PathComparer = StringComparer.OrdinalIgnoreCase;

    [NotNull]
    public static FileInfoEqualityComparer Default { get; } = new FileInfoEqualityComparer();

    public bool Equals(IFileInfo x, IFileInfo y) => Equals(x?.Path, y?.Path);

    public int GetHashCode(IFileInfo obj) => GetHashCode(obj.Path);

    public bool Equals(string x, string y) => PathComparer.Equals(x, y);

    public int GetHashCode(string obj) => PathComparer.GetHashCode(obj);
    }




    Example



    As an example I use one of my tests that checks whether the relative and embedded files providers do their job correctly.



    [TestClass]
    public class RelativeFileProviderTest
    {
    [TestMethod]
    public void GetFileInfo_DoesNotGetNonExistingEmbeddedFile()
    {
    var fileProvider =
    new RelativeFileProvider(
    new EmbeddedFileProvider(typeof(RelativeFileProviderTest).Assembly),
    @"relativepath");

    var file = fileProvider.GetFileInfo(@"file.ext");

    Assert.IsFalse(file.Exists);
    Assert.IsTrue(SoftString.Comparer.Equals(@"ReusableTestsrelativepathfile.ext", file.Path));
    }
    }




    Questions



    Besides of the default question about can this be improved in anyway I have more:




    • should I be concerned about thread-safty here? I didn't use any locks but adding them isn't a big deal. Should I? Where would you add them? I guess creating files and directories could be good candidates, right?

    • should the EmbeddedFileProvider use the RelativeFileProvider to add the assembly namespace to the path or should I leave it as is?










    share|improve this question
























      up vote
      5
      down vote

      favorite
      1









      up vote
      5
      down vote

      favorite
      1






      1





      When using dependency injection for nearly everything it's good to have some file access abstraction. I find the idea of ASP.NET Core FileProvider nice but not sufficient for my needs so inspired by that I decided to create my own with some more functionality.





      Interfaces



      I have two interfaces that are called just like theirs but they have different members and also other names.



      The first interface represents a single file or a directory.



      [PublicAPI]
      public interface IFileInfo : IEquatable<IFileInfo>, IEquatable<string>
      {
      [NotNull]
      string Path { get; }

      [NotNull]
      string Name { get; }

      bool Exists { get; }

      long Length { get; }

      DateTime ModifiedOn { get; }

      bool IsDirectory { get; }

      [NotNull]
      Stream CreateReadStream();
      }


      The other interface allows me to perform four of the basic file/directory operations:



      [PublicAPI]
      public interface IFileProvider
      {
      [NotNull]
      IFileInfo GetFileInfo([NotNull] string path);

      [NotNull]
      IFileInfo CreateDirectory([NotNull] string path);

      [NotNull]
      IFileInfo DeleteDirectory([NotNull] string path, bool recursive);

      [NotNull]
      Task<IFileInfo> CreateFileAsync([NotNull] string path, [NotNull] Stream data);

      [NotNull]
      IFileInfo DeleteFile([NotNull] string path);
      }




      On top of them I've build three providers:





      • PhysicalFileProvider and PhysicalFileInfo - used for operation on the physical drive


      • EmbeddedFileProvider and EmbeddedFileInfo - used for reading of embedded resources (primarily for testing); internally, it automatically adds the root namespace of the specified assembly to the path


      • InMemoryFileProvider and InMemoryFileInfo - used for testing or runtime data


      There is no GetDirectoryContents API because this is what I have the DirectoryTree for.



      All providers use the same path schema, this is, with the backslash . This is also why the EmbeddedFileProvider does some additional converting between the usual patch and the resource path which is separated by dots .





      Implementations



      So here they are, the three pairs, in the same order as the above list:



      [PublicAPI]
      public class PhysicalFileProvider : IFileProvider
      {
      public IFileInfo GetFileInfo(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      return new PhysicalFileInfo(path);
      }

      public IFileInfo CreateDirectory(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      if (Directory.Exists(path))
      {
      return new PhysicalFileInfo(path);
      }

      try
      {
      var newDirectory = Directory.CreateDirectory(path);
      return new PhysicalFileInfo(newDirectory.FullName);
      }
      catch (Exception ex)
      {
      throw new CreateDirectoryException(path, ex);
      }
      }

      public async Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      try
      {
      using (var fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
      {
      await data.CopyToAsync(fileStream);
      await fileStream.FlushAsync();
      }
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new CreateFileException(path, ex);
      }
      }

      public IFileInfo DeleteFile(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      try
      {
      File.Delete(path);
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new DeleteFileException(path, ex);
      }
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      try
      {
      Directory.Delete(path, recursive);
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new DeleteDirectoryException(path, ex);
      }
      }
      }

      [PublicAPI]
      internal class PhysicalFileInfo : IFileInfo
      {
      public PhysicalFileInfo([NotNull] string path) => Path = path ?? throw new ArgumentNullException(nameof(path));

      #region IFileInfo

      public string Path { get; }

      public string Name => System.IO.Path.GetFileName(Path);

      public bool Exists => File.Exists(Path) || Directory.Exists(Path);

      public long Length => Exists && !IsDirectory ? new FileInfo(Path).Length : -1;

      public DateTime ModifiedOn => !string.IsNullOrEmpty(Path) ? File.GetLastWriteTime(Path) : default;

      public bool IsDirectory => Directory.Exists(Path);

      public Stream CreateReadStream()
      {
      return
      IsDirectory
      ? throw new InvalidOperationException($"Cannot open '{Path}' for reading because it's a directory.")
      : Exists
      ? File.OpenRead(Path)
      : throw new InvalidOperationException("Cannot open '{Path}' for reading because the file does not exist.");
      }

      #endregion

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      public class EmbeddedFileProvider : IFileProvider
      {
      private readonly Assembly _assembly;

      public EmbeddedFileProvider([NotNull] Assembly assembly)
      {
      _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
      BasePath = _assembly.GetName().Name.Replace('.', '\');
      }

      public string BasePath { get; }

      public IFileInfo GetFileInfo(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      // Embedded resouce names are separated by '.' so replace the windows separator.
      var fullName = Path.Combine(BasePath, path).Replace('\', '.');

      // Embedded resource names are case sensitive so find the actual name of the resource.
      var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
      var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);

      return new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream);
      }

      // Convert path back to windows format but the last '.' - this is the file extension.
      private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", "\");

      public IFileInfo CreateDirectory(string path)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory creation.");
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory deletion.");
      }

      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file creation.");
      }

      public IFileInfo DeleteFile(string path)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file deletion.");
      }
      }

      internal class EmbeddedFileInfo : IFileInfo
      {
      private readonly Func<Stream> _getManifestResourceStream;

      public EmbeddedFileInfo(string path, Func<Stream> getManifestResourceStream)
      {
      _getManifestResourceStream = getManifestResourceStream;
      Path = path;
      }

      public string Path { get; }

      public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

      public bool Exists => !(_getManifestResourceStream is null);

      public long Length => _getManifestResourceStream()?.Length ?? -1;

      public DateTime ModifiedOn { get; }

      public bool IsDirectory => false;

      // No protection necessary because there are no embedded directories.
      public Stream CreateReadStream() => _getManifestResourceStream();

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      public class InMemoryFileProvider : Dictionary<string, byte>, IFileProvider
      {
      private readonly ISet<IFileInfo> _files = new HashSet<IFileInfo>();

      #region IFileProvider

      public IFileInfo GetFileInfo(string path)
      {
      var file = _files.SingleOrDefault(f => FileInfoEqualityComparer.Default.Equals(f.Path, path));
      return file ?? new InMemoryFileInfo(path, default(byte));
      }

      public IFileInfo CreateDirectory(string path)
      {
      path = path.TrimEnd('\');
      var newDirectory = new InMemoryFileInfo(path, _files.Where(f => f.Path.StartsWith(path)));
      _files.Add(newDirectory);
      return newDirectory;
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      return DeleteFile(path);

      }
      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      var file = new InMemoryFileInfo(path, GetByteArray(data));
      _files.Remove(file);
      _files.Add(file);
      return Task.FromResult<IFileInfo>(file);

      byte GetByteArray(Stream stream)
      {
      using (var memoryStream = new MemoryStream())
      {
      stream.CopyTo(memoryStream);
      return memoryStream.ToArray();
      }
      }
      }

      public IFileInfo DeleteFile(string path)
      {
      var fileToDelete = new InMemoryFileInfo(path, default(byte));
      _files.Remove(fileToDelete);
      return fileToDelete;
      }

      #endregion
      }

      internal class InMemoryFileInfo : IFileInfo
      {
      [CanBeNull]
      private readonly byte _data;

      [CanBeNull]
      private readonly IEnumerable<IFileInfo> _files;

      private InMemoryFileInfo([NotNull] string path)
      {
      Path = path ?? throw new ArgumentNullException(nameof(path));
      ModifiedOn = DateTime.UtcNow;
      }

      public InMemoryFileInfo([NotNull] string path, byte data)
      : this(path)
      {
      _data = data;
      Exists = !(data is null);
      IsDirectory = false;
      }

      public InMemoryFileInfo([NotNull] string path, [NotNull] IEnumerable<IFileInfo> files)
      : this(path)
      {
      _files = files ?? throw new ArgumentNullException(nameof(files));
      Exists = true;
      IsDirectory = true;
      }

      #region IFileInfo

      public bool Exists { get; }

      public long Length => IsDirectory ? throw new InvalidOperationException("Directories have no length.") : _data?.Length ?? -1;

      public string Path { get; }

      public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

      public DateTime ModifiedOn { get; }

      public bool IsDirectory { get; }

      public Stream CreateReadStream()
      {
      return
      IsDirectory
      ? throw new InvalidOperationException("Cannot create read-stream for a directory.")
      : Exists
      // ReSharper disable once AssignNullToNotNullAttribute - this is never null because it's protected by Exists.
      ? new MemoryStream(_data)
      : throw new InvalidOperationException("Cannot create a read-stream for a file that does not exist.");
      }

      #endregion

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      Decorator for less typing



      There is one more file provider. I use this to save some typing of paths. The RelativeFileProvider adds its path in front of the other path if there is some root path that doesn't change.



      public class RelativeFileProvider : IFileProvider
      {
      private readonly IFileProvider _fileProvider;

      private readonly string _basePath;

      public RelativeFileProvider([NotNull] IFileProvider fileProvider, [NotNull] string basePath)
      {
      _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
      _basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
      }

      public IFileInfo GetFileInfo(string path) => _fileProvider.GetFileInfo(CreateFullPath(path));

      public IFileInfo CreateDirectory(string path) => _fileProvider.CreateDirectory(CreateFullPath(path));

      public IFileInfo DeleteDirectory(string path, bool recursive) => _fileProvider.DeleteDirectory(CreateFullPath(path), recursive);

      public Task<IFileInfo> CreateFileAsync(string path, Stream data) => _fileProvider.CreateFileAsync(CreateFullPath(path), data);

      public IFileInfo DeleteFile(string path) => _fileProvider.DeleteFile(CreateFullPath(path));

      private string CreateFullPath(string path) => Path.Combine(_basePath, path ?? throw new ArgumentNullException(nameof(path)));
      }




      Exceptions



      The providers don't throw pure .NET exception because they aren't usually helpful. I wrap them in my own types:



      public class CreateDirectoryException : Exception
      {
      public CreateDirectoryException(string path, Exception innerException)
      : base($"Could not create directory: {path}", innerException)
      { }
      }

      public class CreateFileException : Exception
      {
      public CreateFileException(string path, Exception innerException)
      : base($"Could not create file: {path}", innerException)
      { }
      }

      public class DeleteDirectoryException : Exception
      {
      public DeleteDirectoryException(string path, Exception innerException)
      : base($"Could not delete directory: {path}", innerException)
      { }
      }

      public class DeleteFileException : Exception
      {
      public DeleteFileException(string path, Exception innerException)
      : base($"Could not delete file: {path}", innerException)
      { }
      }




      Simple file search



      There is one more provider that allows me to probe multiple providers. It supports only reading:



      public class CompositeFileProvider : IFileProvider
      {
      private readonly IEnumerable<IFileProvider> _fileProviders;

      public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders)
      {
      _fileProviders = fileProviders;
      }

      public IFileInfo GetFileInfo(string path)
      {
      foreach (var fileProvider in _fileProviders)
      {
      var fileInfo = fileProvider.GetFileInfo(path);
      if (fileInfo.Exists)
      {
      return fileInfo;
      }
      }

      return new InMemoryFileInfo(path, new byte[0]);
      }

      public IFileInfo CreateDirectory(string path)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory creation.");
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory deletion.");
      }

      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file creation.");
      }

      public IFileInfo DeleteFile(string path)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file deletion.");
      }
      }




      Comparing files



      The comparer for the IFileInfo is very straightforward and compares the Path property:



      public class FileInfoEqualityComparer : IEqualityComparer<IFileInfo>, IEqualityComparer<string>
      {
      private static readonly IEqualityComparer PathComparer = StringComparer.OrdinalIgnoreCase;

      [NotNull]
      public static FileInfoEqualityComparer Default { get; } = new FileInfoEqualityComparer();

      public bool Equals(IFileInfo x, IFileInfo y) => Equals(x?.Path, y?.Path);

      public int GetHashCode(IFileInfo obj) => GetHashCode(obj.Path);

      public bool Equals(string x, string y) => PathComparer.Equals(x, y);

      public int GetHashCode(string obj) => PathComparer.GetHashCode(obj);
      }




      Example



      As an example I use one of my tests that checks whether the relative and embedded files providers do their job correctly.



      [TestClass]
      public class RelativeFileProviderTest
      {
      [TestMethod]
      public void GetFileInfo_DoesNotGetNonExistingEmbeddedFile()
      {
      var fileProvider =
      new RelativeFileProvider(
      new EmbeddedFileProvider(typeof(RelativeFileProviderTest).Assembly),
      @"relativepath");

      var file = fileProvider.GetFileInfo(@"file.ext");

      Assert.IsFalse(file.Exists);
      Assert.IsTrue(SoftString.Comparer.Equals(@"ReusableTestsrelativepathfile.ext", file.Path));
      }
      }




      Questions



      Besides of the default question about can this be improved in anyway I have more:




      • should I be concerned about thread-safty here? I didn't use any locks but adding them isn't a big deal. Should I? Where would you add them? I guess creating files and directories could be good candidates, right?

      • should the EmbeddedFileProvider use the RelativeFileProvider to add the assembly namespace to the path or should I leave it as is?










      share|improve this question













      When using dependency injection for nearly everything it's good to have some file access abstraction. I find the idea of ASP.NET Core FileProvider nice but not sufficient for my needs so inspired by that I decided to create my own with some more functionality.





      Interfaces



      I have two interfaces that are called just like theirs but they have different members and also other names.



      The first interface represents a single file or a directory.



      [PublicAPI]
      public interface IFileInfo : IEquatable<IFileInfo>, IEquatable<string>
      {
      [NotNull]
      string Path { get; }

      [NotNull]
      string Name { get; }

      bool Exists { get; }

      long Length { get; }

      DateTime ModifiedOn { get; }

      bool IsDirectory { get; }

      [NotNull]
      Stream CreateReadStream();
      }


      The other interface allows me to perform four of the basic file/directory operations:



      [PublicAPI]
      public interface IFileProvider
      {
      [NotNull]
      IFileInfo GetFileInfo([NotNull] string path);

      [NotNull]
      IFileInfo CreateDirectory([NotNull] string path);

      [NotNull]
      IFileInfo DeleteDirectory([NotNull] string path, bool recursive);

      [NotNull]
      Task<IFileInfo> CreateFileAsync([NotNull] string path, [NotNull] Stream data);

      [NotNull]
      IFileInfo DeleteFile([NotNull] string path);
      }




      On top of them I've build three providers:





      • PhysicalFileProvider and PhysicalFileInfo - used for operation on the physical drive


      • EmbeddedFileProvider and EmbeddedFileInfo - used for reading of embedded resources (primarily for testing); internally, it automatically adds the root namespace of the specified assembly to the path


      • InMemoryFileProvider and InMemoryFileInfo - used for testing or runtime data


      There is no GetDirectoryContents API because this is what I have the DirectoryTree for.



      All providers use the same path schema, this is, with the backslash . This is also why the EmbeddedFileProvider does some additional converting between the usual patch and the resource path which is separated by dots .





      Implementations



      So here they are, the three pairs, in the same order as the above list:



      [PublicAPI]
      public class PhysicalFileProvider : IFileProvider
      {
      public IFileInfo GetFileInfo(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      return new PhysicalFileInfo(path);
      }

      public IFileInfo CreateDirectory(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      if (Directory.Exists(path))
      {
      return new PhysicalFileInfo(path);
      }

      try
      {
      var newDirectory = Directory.CreateDirectory(path);
      return new PhysicalFileInfo(newDirectory.FullName);
      }
      catch (Exception ex)
      {
      throw new CreateDirectoryException(path, ex);
      }
      }

      public async Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      try
      {
      using (var fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
      {
      await data.CopyToAsync(fileStream);
      await fileStream.FlushAsync();
      }
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new CreateFileException(path, ex);
      }
      }

      public IFileInfo DeleteFile(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      try
      {
      File.Delete(path);
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new DeleteFileException(path, ex);
      }
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      try
      {
      Directory.Delete(path, recursive);
      return new PhysicalFileInfo(path);
      }
      catch (Exception ex)
      {
      throw new DeleteDirectoryException(path, ex);
      }
      }
      }

      [PublicAPI]
      internal class PhysicalFileInfo : IFileInfo
      {
      public PhysicalFileInfo([NotNull] string path) => Path = path ?? throw new ArgumentNullException(nameof(path));

      #region IFileInfo

      public string Path { get; }

      public string Name => System.IO.Path.GetFileName(Path);

      public bool Exists => File.Exists(Path) || Directory.Exists(Path);

      public long Length => Exists && !IsDirectory ? new FileInfo(Path).Length : -1;

      public DateTime ModifiedOn => !string.IsNullOrEmpty(Path) ? File.GetLastWriteTime(Path) : default;

      public bool IsDirectory => Directory.Exists(Path);

      public Stream CreateReadStream()
      {
      return
      IsDirectory
      ? throw new InvalidOperationException($"Cannot open '{Path}' for reading because it's a directory.")
      : Exists
      ? File.OpenRead(Path)
      : throw new InvalidOperationException("Cannot open '{Path}' for reading because the file does not exist.");
      }

      #endregion

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      public class EmbeddedFileProvider : IFileProvider
      {
      private readonly Assembly _assembly;

      public EmbeddedFileProvider([NotNull] Assembly assembly)
      {
      _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
      BasePath = _assembly.GetName().Name.Replace('.', '\');
      }

      public string BasePath { get; }

      public IFileInfo GetFileInfo(string path)
      {
      if (path == null) throw new ArgumentNullException(nameof(path));

      // Embedded resouce names are separated by '.' so replace the windows separator.
      var fullName = Path.Combine(BasePath, path).Replace('\', '.');

      // Embedded resource names are case sensitive so find the actual name of the resource.
      var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
      var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);

      return new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream);
      }

      // Convert path back to windows format but the last '.' - this is the file extension.
      private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", "\");

      public IFileInfo CreateDirectory(string path)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory creation.");
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support directory deletion.");
      }

      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file creation.");
      }

      public IFileInfo DeleteFile(string path)
      {
      throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support file deletion.");
      }
      }

      internal class EmbeddedFileInfo : IFileInfo
      {
      private readonly Func<Stream> _getManifestResourceStream;

      public EmbeddedFileInfo(string path, Func<Stream> getManifestResourceStream)
      {
      _getManifestResourceStream = getManifestResourceStream;
      Path = path;
      }

      public string Path { get; }

      public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

      public bool Exists => !(_getManifestResourceStream is null);

      public long Length => _getManifestResourceStream()?.Length ?? -1;

      public DateTime ModifiedOn { get; }

      public bool IsDirectory => false;

      // No protection necessary because there are no embedded directories.
      public Stream CreateReadStream() => _getManifestResourceStream();

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      public class InMemoryFileProvider : Dictionary<string, byte>, IFileProvider
      {
      private readonly ISet<IFileInfo> _files = new HashSet<IFileInfo>();

      #region IFileProvider

      public IFileInfo GetFileInfo(string path)
      {
      var file = _files.SingleOrDefault(f => FileInfoEqualityComparer.Default.Equals(f.Path, path));
      return file ?? new InMemoryFileInfo(path, default(byte));
      }

      public IFileInfo CreateDirectory(string path)
      {
      path = path.TrimEnd('\');
      var newDirectory = new InMemoryFileInfo(path, _files.Where(f => f.Path.StartsWith(path)));
      _files.Add(newDirectory);
      return newDirectory;
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      return DeleteFile(path);

      }
      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      var file = new InMemoryFileInfo(path, GetByteArray(data));
      _files.Remove(file);
      _files.Add(file);
      return Task.FromResult<IFileInfo>(file);

      byte GetByteArray(Stream stream)
      {
      using (var memoryStream = new MemoryStream())
      {
      stream.CopyTo(memoryStream);
      return memoryStream.ToArray();
      }
      }
      }

      public IFileInfo DeleteFile(string path)
      {
      var fileToDelete = new InMemoryFileInfo(path, default(byte));
      _files.Remove(fileToDelete);
      return fileToDelete;
      }

      #endregion
      }

      internal class InMemoryFileInfo : IFileInfo
      {
      [CanBeNull]
      private readonly byte _data;

      [CanBeNull]
      private readonly IEnumerable<IFileInfo> _files;

      private InMemoryFileInfo([NotNull] string path)
      {
      Path = path ?? throw new ArgumentNullException(nameof(path));
      ModifiedOn = DateTime.UtcNow;
      }

      public InMemoryFileInfo([NotNull] string path, byte data)
      : this(path)
      {
      _data = data;
      Exists = !(data is null);
      IsDirectory = false;
      }

      public InMemoryFileInfo([NotNull] string path, [NotNull] IEnumerable<IFileInfo> files)
      : this(path)
      {
      _files = files ?? throw new ArgumentNullException(nameof(files));
      Exists = true;
      IsDirectory = true;
      }

      #region IFileInfo

      public bool Exists { get; }

      public long Length => IsDirectory ? throw new InvalidOperationException("Directories have no length.") : _data?.Length ?? -1;

      public string Path { get; }

      public string Name => System.IO.Path.GetFileNameWithoutExtension(Path);

      public DateTime ModifiedOn { get; }

      public bool IsDirectory { get; }

      public Stream CreateReadStream()
      {
      return
      IsDirectory
      ? throw new InvalidOperationException("Cannot create read-stream for a directory.")
      : Exists
      // ReSharper disable once AssignNullToNotNullAttribute - this is never null because it's protected by Exists.
      ? new MemoryStream(_data)
      : throw new InvalidOperationException("Cannot create a read-stream for a file that does not exist.");
      }

      #endregion

      #region IEquatable<IFileInfo>

      public override bool Equals(object obj) => obj is IFileInfo file && Equals(file);

      public bool Equals(IFileInfo other) => FileInfoEqualityComparer.Default.Equals(other, this);

      public bool Equals(string other) => FileInfoEqualityComparer.Default.Equals(other, Path);

      public override int GetHashCode() => FileInfoEqualityComparer.Default.GetHashCode(this);

      #endregion
      }




      Decorator for less typing



      There is one more file provider. I use this to save some typing of paths. The RelativeFileProvider adds its path in front of the other path if there is some root path that doesn't change.



      public class RelativeFileProvider : IFileProvider
      {
      private readonly IFileProvider _fileProvider;

      private readonly string _basePath;

      public RelativeFileProvider([NotNull] IFileProvider fileProvider, [NotNull] string basePath)
      {
      _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
      _basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
      }

      public IFileInfo GetFileInfo(string path) => _fileProvider.GetFileInfo(CreateFullPath(path));

      public IFileInfo CreateDirectory(string path) => _fileProvider.CreateDirectory(CreateFullPath(path));

      public IFileInfo DeleteDirectory(string path, bool recursive) => _fileProvider.DeleteDirectory(CreateFullPath(path), recursive);

      public Task<IFileInfo> CreateFileAsync(string path, Stream data) => _fileProvider.CreateFileAsync(CreateFullPath(path), data);

      public IFileInfo DeleteFile(string path) => _fileProvider.DeleteFile(CreateFullPath(path));

      private string CreateFullPath(string path) => Path.Combine(_basePath, path ?? throw new ArgumentNullException(nameof(path)));
      }




      Exceptions



      The providers don't throw pure .NET exception because they aren't usually helpful. I wrap them in my own types:



      public class CreateDirectoryException : Exception
      {
      public CreateDirectoryException(string path, Exception innerException)
      : base($"Could not create directory: {path}", innerException)
      { }
      }

      public class CreateFileException : Exception
      {
      public CreateFileException(string path, Exception innerException)
      : base($"Could not create file: {path}", innerException)
      { }
      }

      public class DeleteDirectoryException : Exception
      {
      public DeleteDirectoryException(string path, Exception innerException)
      : base($"Could not delete directory: {path}", innerException)
      { }
      }

      public class DeleteFileException : Exception
      {
      public DeleteFileException(string path, Exception innerException)
      : base($"Could not delete file: {path}", innerException)
      { }
      }




      Simple file search



      There is one more provider that allows me to probe multiple providers. It supports only reading:



      public class CompositeFileProvider : IFileProvider
      {
      private readonly IEnumerable<IFileProvider> _fileProviders;

      public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders)
      {
      _fileProviders = fileProviders;
      }

      public IFileInfo GetFileInfo(string path)
      {
      foreach (var fileProvider in _fileProviders)
      {
      var fileInfo = fileProvider.GetFileInfo(path);
      if (fileInfo.Exists)
      {
      return fileInfo;
      }
      }

      return new InMemoryFileInfo(path, new byte[0]);
      }

      public IFileInfo CreateDirectory(string path)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory creation.");
      }

      public IFileInfo DeleteDirectory(string path, bool recursive)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support directory deletion.");
      }

      public Task<IFileInfo> CreateFileAsync(string path, Stream data)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file creation.");
      }

      public IFileInfo DeleteFile(string path)
      {
      throw new NotSupportedException($"{nameof(CompositeFileProvider)} does not support file deletion.");
      }
      }




      Comparing files



      The comparer for the IFileInfo is very straightforward and compares the Path property:



      public class FileInfoEqualityComparer : IEqualityComparer<IFileInfo>, IEqualityComparer<string>
      {
      private static readonly IEqualityComparer PathComparer = StringComparer.OrdinalIgnoreCase;

      [NotNull]
      public static FileInfoEqualityComparer Default { get; } = new FileInfoEqualityComparer();

      public bool Equals(IFileInfo x, IFileInfo y) => Equals(x?.Path, y?.Path);

      public int GetHashCode(IFileInfo obj) => GetHashCode(obj.Path);

      public bool Equals(string x, string y) => PathComparer.Equals(x, y);

      public int GetHashCode(string obj) => PathComparer.GetHashCode(obj);
      }




      Example



      As an example I use one of my tests that checks whether the relative and embedded files providers do their job correctly.



      [TestClass]
      public class RelativeFileProviderTest
      {
      [TestMethod]
      public void GetFileInfo_DoesNotGetNonExistingEmbeddedFile()
      {
      var fileProvider =
      new RelativeFileProvider(
      new EmbeddedFileProvider(typeof(RelativeFileProviderTest).Assembly),
      @"relativepath");

      var file = fileProvider.GetFileInfo(@"file.ext");

      Assert.IsFalse(file.Exists);
      Assert.IsTrue(SoftString.Comparer.Equals(@"ReusableTestsrelativepathfile.ext", file.Path));
      }
      }




      Questions



      Besides of the default question about can this be improved in anyway I have more:




      • should I be concerned about thread-safty here? I didn't use any locks but adding them isn't a big deal. Should I? Where would you add them? I guess creating files and directories could be good candidates, right?

      • should the EmbeddedFileProvider use the RelativeFileProvider to add the assembly namespace to the path or should I leave it as is?







      c# file-system dependency-injection






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked 14 hours ago









      t3chb0t

      33.3k744106




      33.3k744106






















          1 Answer
          1






          active

          oldest

          votes

















          up vote
          2
          down vote













          In PhysicalFileProvider.CreateDirectory() you should place the call to Directory.Exists() inside the try..catch as well because it can throw e.g ArgumentException for "C:test?" or a NotSupportedException for C::.



          But basically you could skip this check at all because calling Directory.CreateDirectory() will do the check itself (Directory.CreateDiractory reference source).



          In PhysicalFileInfo.ModifiedOn you should change the check from !string.IsNullOrEmpty(Path) to !string.IsNullOrWhiteSpace(Path) to avoid an ArgumentException if Path only contains whitespace characters.

          Well basically you should validate the path in your ctor some more, e.g for illigal characters etc. to avoid your methods to throw exceptions.



          Otherwise your code looks clean as usual and is easy to understand. At least PhysicalFileInfo and PhysicalFileProvider are thread-safe because you don't change any class level state outside of the ctor.



          A small nitpick: Regions are smelling.






          share|improve this answer





















          • Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
            – t3chb0t
            1 hour ago










          • Ok, but thats the last time ;-)
            – Heslacher
            57 mins ago











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














           

          draft saved


          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207585%2fmultiple-file-access-abstractions%23new-answer', 'question_page');
          }
          );

          Post as a guest
































          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          up vote
          2
          down vote













          In PhysicalFileProvider.CreateDirectory() you should place the call to Directory.Exists() inside the try..catch as well because it can throw e.g ArgumentException for "C:test?" or a NotSupportedException for C::.



          But basically you could skip this check at all because calling Directory.CreateDirectory() will do the check itself (Directory.CreateDiractory reference source).



          In PhysicalFileInfo.ModifiedOn you should change the check from !string.IsNullOrEmpty(Path) to !string.IsNullOrWhiteSpace(Path) to avoid an ArgumentException if Path only contains whitespace characters.

          Well basically you should validate the path in your ctor some more, e.g for illigal characters etc. to avoid your methods to throw exceptions.



          Otherwise your code looks clean as usual and is easy to understand. At least PhysicalFileInfo and PhysicalFileProvider are thread-safe because you don't change any class level state outside of the ctor.



          A small nitpick: Regions are smelling.






          share|improve this answer





















          • Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
            – t3chb0t
            1 hour ago










          • Ok, but thats the last time ;-)
            – Heslacher
            57 mins ago















          up vote
          2
          down vote













          In PhysicalFileProvider.CreateDirectory() you should place the call to Directory.Exists() inside the try..catch as well because it can throw e.g ArgumentException for "C:test?" or a NotSupportedException for C::.



          But basically you could skip this check at all because calling Directory.CreateDirectory() will do the check itself (Directory.CreateDiractory reference source).



          In PhysicalFileInfo.ModifiedOn you should change the check from !string.IsNullOrEmpty(Path) to !string.IsNullOrWhiteSpace(Path) to avoid an ArgumentException if Path only contains whitespace characters.

          Well basically you should validate the path in your ctor some more, e.g for illigal characters etc. to avoid your methods to throw exceptions.



          Otherwise your code looks clean as usual and is easy to understand. At least PhysicalFileInfo and PhysicalFileProvider are thread-safe because you don't change any class level state outside of the ctor.



          A small nitpick: Regions are smelling.






          share|improve this answer





















          • Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
            – t3chb0t
            1 hour ago










          • Ok, but thats the last time ;-)
            – Heslacher
            57 mins ago













          up vote
          2
          down vote










          up vote
          2
          down vote









          In PhysicalFileProvider.CreateDirectory() you should place the call to Directory.Exists() inside the try..catch as well because it can throw e.g ArgumentException for "C:test?" or a NotSupportedException for C::.



          But basically you could skip this check at all because calling Directory.CreateDirectory() will do the check itself (Directory.CreateDiractory reference source).



          In PhysicalFileInfo.ModifiedOn you should change the check from !string.IsNullOrEmpty(Path) to !string.IsNullOrWhiteSpace(Path) to avoid an ArgumentException if Path only contains whitespace characters.

          Well basically you should validate the path in your ctor some more, e.g for illigal characters etc. to avoid your methods to throw exceptions.



          Otherwise your code looks clean as usual and is easy to understand. At least PhysicalFileInfo and PhysicalFileProvider are thread-safe because you don't change any class level state outside of the ctor.



          A small nitpick: Regions are smelling.






          share|improve this answer












          In PhysicalFileProvider.CreateDirectory() you should place the call to Directory.Exists() inside the try..catch as well because it can throw e.g ArgumentException for "C:test?" or a NotSupportedException for C::.



          But basically you could skip this check at all because calling Directory.CreateDirectory() will do the check itself (Directory.CreateDiractory reference source).



          In PhysicalFileInfo.ModifiedOn you should change the check from !string.IsNullOrEmpty(Path) to !string.IsNullOrWhiteSpace(Path) to avoid an ArgumentException if Path only contains whitespace characters.

          Well basically you should validate the path in your ctor some more, e.g for illigal characters etc. to avoid your methods to throw exceptions.



          Otherwise your code looks clean as usual and is easy to understand. At least PhysicalFileInfo and PhysicalFileProvider are thread-safe because you don't change any class level state outside of the ctor.



          A small nitpick: Regions are smelling.







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered 1 hour ago









          Heslacher

          44.4k460153




          44.4k460153












          • Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
            – t3chb0t
            1 hour ago










          • Ok, but thats the last time ;-)
            – Heslacher
            57 mins ago


















          • Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
            – t3chb0t
            1 hour ago










          • Ok, but thats the last time ;-)
            – Heslacher
            57 mins ago
















          Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
          – t3chb0t
          1 hour ago




          Regions are smelling. - this is the only case where I actually use regions, to group interface members or sometimes operator overloads, other than this I dislike them too - will you forgive me for that? ;-]
          – t3chb0t
          1 hour ago












          Ok, but thats the last time ;-)
          – Heslacher
          57 mins ago




          Ok, but thats the last time ;-)
          – Heslacher
          57 mins ago


















           

          draft saved


          draft discarded



















































           


          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207585%2fmultiple-file-access-abstractions%23new-answer', 'question_page');
          }
          );

          Post as a guest




















































































          Popular posts from this blog

          List directoties down one level, excluding some named directories and files

          list processes belonging to a network namespace

          list systemd RuntimeDirectory mounts