4
\$\begingroup\$

I've got pretty straight forward scenario. I've got a class called FileInformationExtractor and method GetFilesInformation that takes directory path to find files within it and yields specific information about each of them. Exceptions I consider here that should be handled are IOException and UnauthorizedAccessException. Now I don't want to break enumeration so I've got try/catch block within the loop to process all the files. On the other hand I don't want to pretend that nothing happened and family's still happy so first I considered logging, but since this is quite low level class in my system I'd hate to reference log library from basicaly IO extension library. The solution I came up with is to first handle the exceptions, but instead of loosing them I collect them inside the List<Exception and throw as AggregateException after enumeration is complete. I'd like to know if you have any suggestions for alternative approaches, perhaps something I could improve about the code itself, good practices and so on.

/// <summary>
/// Provides information about files
/// </summary>
public class FileInformationExtractor : IFileInformationExtractor
{
    private readonly IFileFinder _fileFinder;
    private readonly IFileHelper _fileHelper;
    private readonly IFileHasher _hasher;

    /// <summary>
    /// Default constructor.
    /// </summary>
    /// <param name="fileFinder">Movie finder</param>
    /// <param name="fileHelper">File helper</param>
    /// <param name="hasher">File hasher</param>
    /// <exception cref="T:System.ArgumentNullException">Throws when any of constructor parameters is null.</exception>
    public FileInformationExtractor( IFileFinder fileFinder, IFileHelper fileHelper, IFileHasher hasher)
    {
        if ( fileFinder == null ) throw new ArgumentNullException( nameof( fileFinder ) );
        if ( fileHelper == null ) throw new ArgumentNullException( nameof( fileHelper ) );
        if ( hasher == null ) throw new ArgumentNullException( nameof( hasher ) );

        _fileFinder = fileFinder;
        _fileHelper = fileHelper;
        _hasher = hasher;
    }

    /// <summary>
    /// Searches given directory looking for movie files, generates their hash. 
    /// </summary>
    /// <param name="path">Directory</param>
    /// <returns>Collection of FileInformation</returns>
    /// <exception cref="T:System.AggregateException">Throws after enumeration is complete 
    /// if any exceptions were handled during it.</exception>
    public IEnumerable<FileInformation> GetFilesInformation( string path )
    {
        var files = _fileFinder.Find( path, true );
        var handledExceptins = new List<Exception>();

        foreach ( var filePath in files )
        {                
            var fileInformation = new FileInformation();
            try
            {
                var hash = _hasher.GenerateHash( filePath );
                fileInformation.Path = filePath;
                fileInformation.Hash = hash;
            }
            catch ( UnauthorizedAccessException exception )
            {
                handledExceptins.Add( exception );
            }
            catch ( IOException exception )
            {
                handledExceptins.Add( exception );
            }

            if ( IsInformationNotEmpty( fileInformation ) )
            {
                yield return fileInformation;
            }
        }

        if ( handledExceptins.Count > 0 )
        {
            throw new AggregateException(handledExceptins);
        }
    }

    private bool IsInformationNotEmpty( FileInformation fileInformation )
    {
        return !string.IsNullOrWhiteSpace( fileInformation.Hash );
    }
}

Apparently writing the problem down helped me out a little. I recalled TryGet... kind of methods, that handle exceptions and use out to get the result so I modified code a little introducing custom exception. I like that approach better than previous one. Should I worry about other exceptions? I'm also really looking forward for any naming suggestions (figuring out how to call all the stuff takes me ages :P).

/// <summary>
/// Provides information about files
/// </summary>
public class FileInformationExtractor : IFileInformationExtractor
{
    private readonly IFileFinder _fileFinder;
    private readonly IFileHelper _fileHelper;
    private readonly IFileHasher _hasher;

    /// <summary>
    /// Default constructor.
    /// </summary>
    /// <param name="fileFinder">Movie finder</param>
    /// <param name="fileHelper">File helper</param>
    /// <param name="hasher">File hasher</param>
    /// <exception cref="T:System.ArgumentNullException">Throws when any of constructor parameters is null.</exception>
    public FileInformationExtractor( IFileFinder fileFinder, IFileHelper fileHelper, IFileHasher hasher)
    {
        if ( fileFinder == null ) throw new ArgumentNullException( nameof( fileFinder ) );
        if ( fileHelper == null ) throw new ArgumentNullException( nameof( fileHelper ) );
        if ( hasher == null ) throw new ArgumentNullException( nameof( hasher ) );

        _fileFinder = fileFinder;
        _fileHelper = fileHelper;
        _hasher = hasher;
    }

    /// <summary>
    /// Searches given directory looking for movie files, generates their hash. 
    /// </summary>
    /// <param name="path">Directory</param>
    /// <returns>Collection of FileInformation</returns>
    /// <exception cref="T:Common.IO.FileHashingException">Throws after enumeration is complete 
    /// if any exceptions were handled during it.</exception>
    public IEnumerable<FileInformation> GetFilesInformation( string path )
    {
        var files = _fileFinder.Find( path, true );
        var notProcessedFiles = new List<string>();

        foreach ( var filePath in files )
        {
            FileInformation fileInformation;

            if ( TryGetFileInformation( filePath, out fileInformation ) )
            {
                yield return fileInformation;
            }
            else
            {
                notProcessedFiles.Add( filePath );
            }
        }

        if ( notProcessedFiles.Count > 0 )
        {
            throw new FileHashingException( notProcessedFiles );
        }
    }

    private bool TryGetFileInformation( string filePath, out FileInformation fileInformation )
    {
        try
        {
            var hash = _hasher.GenerateHash( filePath );

            fileInformation = new FileInformation
            {
                Path = filePath,
                Hash = hash
            };

            return true;
        }
        catch (IOException e)
        {
            fileInformation = null;
            return false;
        }
        catch (UnauthorizedAccessException e)
        {
            fileInformation = null;
            return false;
        }
    }
}

public class FileHashingException : Exception
{
    private const string ExceptionMessage = "There were exceptions when trying to hash listed files.";
    public IEnumerable<string> FilesCausingException { get; }

    public FileHashingException( IEnumerable<string> filesCausingException ) 
        : base(ExceptionMessage)
    {
        FilesCausingException = filesCausingException;
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Is the return type of _fileFinder.Find IEnumerable<string>? \$\endgroup\$ Commented Sep 21, 2016 at 8:11
  • 1
    \$\begingroup\$ Yes, it returns paths of the files as IEnumerable<string>. \$\endgroup\$
    – yoger
    Commented Sep 21, 2016 at 8:16

3 Answers 3

5
\$\begingroup\$

I think you might be over-designing.

Let's start by taking a look to the class dependencies:

private readonly IFileFinder _fileFinder;
private readonly IFileHelper _fileHelper;
private readonly IFileHasher _hasher;

_fileHelper is not used anywhere and can be removed. The need of _hasher is questionable unless what you want to do here is to hash the contents from the file. Probably you can live with calling GetHashCode() of the file path which is of the string type, which has a really good hashing algorithm. That only takes 1 line of code.

It seems that, in some way, the responsibility of the current class could be moved into IFileFinder, so that interface could return FileInformation on the Find method instead of just returning strings. However I can live with that dependency if it makes some sense somehow, so I leave that decision up to you.

If I take a look closely to GetFilesInformation I can't see why it should throw any exception at all. All you are doing there is creating a hash and putting the file path on a property. So again, unless the _hasher hashes the contents of a file there shouldn't be any exception.

One thing that you can do is to create a variable and check if it was assigned or not, yes you have to make sure that you call the methods that can fail first but well, I guess it's quite understandable and a easy solution in this scenario. You can also extract the code to another method. The implementation it's quite similar to the one provided by @Heslacher, except it returns null on exception. I prefer that way because I hate to write methods with out and ref keywords (I always will, it's my preference it might be even be a narrow minded one, but that does not bother me).

A better alternative in my perspective could be to support an event that calls the subscribers when an error occurs:

public class FileErrorArgs : EventArgs{
    public IEnumerable<string> Files{ get; set; }
}
public event EventHandler<FileErrorArgs> FileError;

public IEnumerable<FileInformation> GetFilesInformation( string path )
{
    var files = _fileFinder.Find( path, true );
    var invalidFiles = new List<string>();

    foreach ( var filePath in files )
    {                
        FileInformation fileInformation = GetFileInformation(string filePath);
        if(fileInformation == null){
            invalidFiles.Add(filePath);
        }else{
            yield return fileInformation;
        }
    }

    if ( invalidFiles.Count > 0 )
    {
        var handler = FileError;
        if(handler != null){
            handler.Invoke(new FileErrorArgs{
                Files = invalidFiles
            });
        }
    }
}

public FileInformation GetFileInformation(string filePath){
    try
    {
        return new FileInformation(){
            Hash = _hasher.GenerateHash(filePath),
            Path = filePath
        };
    }
    catch ( UnauthorizedAccessException exception )
    {
        return null;
    }
    catch ( IOException exception )
    {
        return null;
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Good catch with IFileHelper , I forgot to remove it after moving it to lower layer. IFileHasher actually generates hashcode of entire file using external algorithm and IFileFinder implementation returns just paths to files with specified extension, digging subdirectories etc. So I considered that FileInformationExtractor will just take those paths and combine them with IFileHashers output. I'm really glad you mentioned the events, I completly skipped that possibility for some reason. One thing though, you can't yield within try/catch block. I'll get back with the code later. \$\endgroup\$
    – yoger
    Commented Sep 21, 2016 at 15:05
  • \$\begingroup\$ @yoger What a shame that does not work, I will edit my answer accordingly. \$\endgroup\$ Commented Sep 21, 2016 at 18:11
4
\$\begingroup\$

GetFilesInformation()

If there is a chance that _fileFinder.Find() throws an exception for the case that the passed in path is either null, string.Empty, non existent or invalid then you should add a private method for doing the yielding and let either GetFilesInformation method take care about the validation of the passed in path. This is because the method in its current start will be on hold until an enumerator is created and the first item is queried.

Something along these lines

public IEnumerable<FileInformation> GetFilesInformation(string path)
{
    if (path == null) { throw new ArgumentNullException("path"); }
    if (string.IsNullOrWhiteSpace(path) { throw new ArgumentException("Path may not be empty or whitespace); }  
    if (!System.IO.Directory.Exists(path)) { throw new DirectoryNotFoundException("The path " + path + " could not be found"); }

    return GetFilesInformationEx(path);
}

private IEnumerable<FileInformation> GetFilesInformationEx(string path)
    var files = _fileFinder.Find(path, true);
    var notProcessedFiles = new List<string>();

    foreach (var filePath in files)
    {
        FileInformation fileInformation;

        if (TryGetFileInformation(filePath, out fileInformation))
        {
            yield return fileInformation;
        }
        else
        {
            notProcessedFiles.Add(filePath);
        }
    }

    if (notProcessedFiles.Count > 0)
    {
        throw new FileHashingException(notProcessedFiles);
    }
}

TryGetFileInformation()

If you set fileInformation to null at the very top, you wouldn't need to repeat it twice (in the catch statements).

like so

private bool TryGetFileInformation(string filePath, out FileInformation fileInformation)
{
    fileInformation = null;

    try
    {
        var hash = _hasher.GenerateHash(filePath);

        fileInformation = new FileInformation
        {
            Path = filePath,
            Hash = hash
        };

        return true;
    }
    catch (IOException e)
    {
        return false;
    }
    catch (UnauthorizedAccessException e)
    {
        return false;
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for pointing out that parameter validation. I knew something important might be missing. \$\endgroup\$
    – yoger
    Commented Sep 21, 2016 at 14:54
2
\$\begingroup\$

Thanks to both of you @Heslacher and @Bruno Costa I've come up with solution I'm quite satisfied with.

First the directory path validation. I decided to keep it within IFileFinder implementation so FileInformationExtractor can be separated from System.IO namespace. When finder will throw, regardless if it's invalid path, argument or none existing directory this class wont handle it, so thats right.

Next thing to achieve full independency was to handle both IOException and UnauthorizedAccessException within IFileHasher implementation, where any of them will be packed inside FileHashingException which FileInformationExtractor can smoothly handle with single catch block. Also @Bruno Costa, I share that distaste with out and ref so I took the null check approach. Exceptions that will be handled are now available through the event. I considered propagating just the message or the path, but having full exception will give me some more flexibility when designing higher level of the application ().

/// <summary>
/// Provides information about files
/// </summary>
public class FileInformationExtractor : IFileInformationExtractor
{
    private readonly IFileFinder _fileFinder;
    private readonly IFileHasher _hasher;

    /// <summary>
    /// Notifies about exceptions during file hashing.
    /// </summary>
    public event EventHandler<FileHashingException> FileHashingFailure;

    /// <summary>
    /// Default constructor.
    /// </summary>
    /// <param name="fileFinder">Movie finder</param>
    /// <param name="hasher">File hasher</param>
    /// <exception cref="T:System.ArgumentNullException">Throws when any of constructor parameters is null.</exception>
    public FileInformationExtractor( IFileFinder fileFinder, IFileHasher hasher)
    {
        if ( fileFinder == null ) throw new ArgumentNullException( nameof( fileFinder ) );
        if ( hasher == null ) throw new ArgumentNullException( nameof( hasher ) );

        _fileFinder = fileFinder;
        _hasher = hasher;
    }

    /// <summary>
    /// Searches given directory looking for movie files, generates their hash. 
    /// </summary>
    /// <param name="path">Directory</param>
    /// <returns>Collection of FileInformation</returns>
    public IEnumerable<FileInformation> GetFilesInformation( string path )
    {
        var files = _fileFinder.Find( path, true );

        foreach (var filePath in files)
        {
            var fileInformation = GetFileInformation( filePath );

            if (fileInformation != null)
            {
                yield return fileInformation;
            }
        }
    }

    private FileInformation GetFileInformation( string filePath )
    {
        try
        {
            var hash = _hasher.GenerateHash( filePath );

            return new FileInformation
            {
                Path = filePath,
                Hash = hash
            };
        }
        catch (FileHashingException e)
        {
            OnFileHashingFailure( e );
            return null;
        }
    }

    protected virtual void OnFileHashingFailure( FileHashingException e )
    {
        FileHashingFailure?.Invoke( this, e );
    }
}

/// <summary>
/// Represents error that occured during while file hashing operation.
/// </summary>
public class FileHashingException : Exception
{
    /// <summary>
    /// Path to the file that caused the exception
    /// </summary>
    public string FilePath { get; }

    /// <summary>
    /// Specific constructor.
    /// </summary>
    /// <param name="filePath">Path</param>
    /// <param name="message">Message</param>
    /// <param name="innerException">Inner exception</param>
    public FileHashingException( string filePath, string message, Exception innerException ) 
        : base(message, innerException)
    {
        FilePath = filePath;
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Yup that's an improvement. Looks good \$\endgroup\$ Commented Sep 21, 2016 at 20:47

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.