Search code examples
jsonvb.netjson.net

How to return Func(Of T, IEnumerable(Of T)) inside a function which handles the exception?


How can I handle the following exception?

My code:

Public Module RecursiveEnumerableExtensions
    'credits Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    Iterator Function Traverse(Of T)(ByVal root As T, ByVal children As Func(Of T, IEnumerable(Of T)), ByVal Optional includeSelf As Boolean = True) As IEnumerable(Of T)
        If includeSelf Then Yield root
        Dim stack = New Stack(Of IEnumerator(Of T))()
        Try
            stack.Push(children(root).GetEnumerator())
            While stack.Count <> 0
                Dim enumerator = stack.Peek()
                If Not enumerator.MoveNext() Then
                    stack.Pop()
                    enumerator.Dispose()
                Else
                    Yield enumerator.Current
                    stack.Push(children(enumerator.Current).GetEnumerator())
                End If
            End While
        Finally
            For Each enumerator In stack
                enumerator.Dispose()
            Next
        End Try
    End Function
End Module

Friend Sub Main()
    Dim dinfo As DirectoryInfo = New DirectoryInfo(curDirectory)
    Dim dquery = RecursiveEnumerableExtensions.Traverse(dinfo, Function(d) d.GetDirectories())
End Sub

When I run my code, for some of the directories, which are not accessible, I receive inside the Friend Sub Main() in the part Function(d) d.GetDirectories()) the exception System.UnauthorizedAccessException. I would like to handle this by Try Catch.

I tried to edit the Friend Sub Main() and export the Lambda expression to a function funcGetDirectories, but it fails with following error: System.InvalidCastException

Friend Sub Main()
    Dim dinfo As DirectoryInfo = New DirectoryInfo(curDirectory)
    Dim dquery = RecursiveEnumerableExtensions.Traverse(dinfo, funcGetDirectories(dinfo))
End Sub

Function funcGetDirectories(di As DirectoryInfo)
    Try
        Return di.GetDirectories()
    Catch ex As UnauthorizedAccessException
        Throw
    Catch ex_default As Exception
        Throw
    End Try
End Function

Whats wrong with my Return di.GetDirectories()?

Note I'm working with .NET 4.8.


Solution

  • One option is to pass in an error handler to your DirectoryInfo and FileSystemAccessRule enumeration methods. Those methods can catch all errors and pass then to the handler, which can optionally handle the error and allow traversal to continue:

    Public Class DirectoryExtensions
        Shared Function GetFileSystemAccessRules(d As DirectoryInfo, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of FileSystemAccessRule)
            Try
                Dim ds As DirectorySecurity = d.GetAccessControl()
                Dim arrRules As AuthorizationRuleCollection = ds.GetAccessRules(True, True, GetType(Security.Principal.NTAccount))
                Return arrRules.Cast(Of FileSystemAccessRule)()
            Catch ex As Exception
                If (Not HandleError(errorHandler, d.FullName, ex))
                    Throw
                End If
                Return Enumerable.Empty(Of FileSystemAccessRule)()
            End Try
        End Function
        
        Shared Function EnumerateDirectories(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of DirectoryInfo)
            Dim di As DirectoryInfo
            Try
                di = new DirectoryInfo(directory)
            Catch ex As Exception
                If (Not HandleError(errorHandler, directory, ex))
                    Throw
                End If
                Return Enumerable.Empty(Of DirectoryInfo)()
            End Try
            ' In .NET Core 2.1+ it should be able to recursively enumerate directories and ignore errors as follows:
            ' Dim query = { di }.Concat(di.EnumerateDirectories("*", New System.IO.EnumerationOptions With { .RecurseSubdirectories = True, .IgnoreInaccessible = True })))
            ' In the meantime, it's necessary to manually catch and ignore errors.
            Dim query = RecursiveEnumerableExtensions.Traverse(di, 
                Function(d)
                    Try
                        Return d.GetDirectories()
                    Catch ex As Exception
                        If (Not HandleError(errorHandler, d.FullName, ex))
                            Throw
                        End If
                        Return Enumerable.Empty(Of DirectoryInfo)()
                    End Try
                End Function
            )
            Return query
        End Function
    
        Shared Function EnumerateDirectoryFileSystemAccessRules(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of Tuple(Of DirectoryInfo, IEnumerable(Of FileSystemAccessRule)))
            Return EnumerateDirectories(directory, errorHandler).Select(Function(d) Tuple.Create(d, GetFileSystemAccessRules(d, errorHandler)))
        End Function
            
        Shared Public Function SerializeFileAccessRules(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs), Optional ByVal formatting As Formatting = Formatting.Indented)
            Dim query = EnumerateDirectoryFileSystemAccessRules(directory, errorHandler).Select(
                Function(tuple) New With {
                    .directory = tuple.Item1.FullName,
                    .permissions = tuple.Item2.Select(
                        Function(a) New With { 
                            .IdentityReference = a.IdentityReference.ToString(),
                            .AccessControlType = a.AccessControlType.ToString(),
                            .FileSystemRights = a.FileSystemRights.ToString(),
                            .IsInherited = a.IsInherited.ToString()
                        }
                    )
                }
            )
            Return JsonConvert.SerializeObject(query, formatting)   
        End Function
                                    
        Private Shared Function HandleError(ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs), ByVal fullName as String, ByVal ex as Exception) As Boolean
            If (errorHandler Is Nothing)
                Return False
            End If
            Dim args As New DirectoryTraversalErrorEventArgs(fullName, ex)
            errorHandler(GetType(DirectoryExtensions), args)
            return args.Handled
        End Function                                
    End Class
    
    Public Class DirectoryTraversalErrorEventArgs 
        Inherits EventArgs
        Private _directory As String
        Private _exception As Exception
    
        Public Sub New(ByVal directory as String, ByVal exception as Exception)
            Me._directory = directory
            Me._exception = exception
        End Sub
        
        Public Property Handled As Boolean = false
        Public Readonly Property Directory As String
            Get
                Return _directory
            End Get
        End Property
        Public Readonly Property Exception As Exception
            Get
                Return _exception
            End Get
        End Property
    End Class
    
    Public Module RecursiveEnumerableExtensions
        ' Translated to vb.net from this answer https://stackoverflow.com/a/60997251/3744182
        ' To https://stackoverflow.com/questions/60994574/how-to-extract-all-values-for-all-jsonproperty-objects-with-a-specified-name-fro
        ' which was rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
        ' to "Efficient graph traversal with LINQ - eliminating recursion" https://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
        Iterator Function Traverse(Of T)(ByVal root As T, ByVal children As Func(Of T, IEnumerable(Of T)), ByVal Optional includeSelf As Boolean = True) As IEnumerable(Of T)
            If includeSelf Then Yield root
            Dim stack = New Stack(Of IEnumerator(Of T))()
    
            Try
                stack.Push(children(root).GetEnumerator())
                While stack.Count <> 0
                    Dim enumerator = stack.Peek()
                    If Not enumerator.MoveNext() Then
                        stack.Pop()
                        enumerator.Dispose()
                    Else
                        Yield enumerator.Current
                        stack.Push(children(enumerator.Current).GetEnumerator())
                    End If
                End While
            Finally
                For Each enumerator In stack
                    enumerator.Dispose()
                Next
            End Try
        End Function
    End Module
    

    Then call the method and accumulate the errors in a list of errors like so:

    Dim errors = New List(Of Tuple(Of String, String))
    Dim handler As Action(Of Object, DirectoryTraversalErrorEventArgs) = 
        Sub(sender, e)
            errors.Add(Tuple.Create(e.Directory, e.Exception.Message))
            e.Handled = true
        End Sub
    Dim json As String = DirectoryExtensions.SerializeFileAccessRules(curDirectory, handler) 
    ' Output the JSON and the errors somehow
    Console.WriteLine(json)
    For Each e In errors
        Console.WriteLine("Error in directory {0}: {1}", e.Item1, e.Item2)
    Next
    

    Notes:

    • I am using tuples in a couple of places. Newer versions of VB.NET have a cleaner syntax for tuples, see Tuples (Visual Basic) for details.

    • The code manually traverses the directory hierarchy by stacking calls to DirectoryInfo.GetDirectories() and trapping errors from each call.

      In .NET Core 2.1+ it should be possible to recursively enumerate directories and ignore errors by using DirectoryInfo.EnumerateDirectories(String, EnumerationOptions) as follows:

      Dim query = { di }.Concat(di.EnumerateDirectories("*", New System.IO.EnumerationOptions With { .RecurseSubdirectories = True, .IgnoreInaccessible = True })))
      

      This overload does not exist in .Net Framework 4.8 though.

    Demo fiddle here