1

The following code gives me the error system.argumentexception an element with the same key already exists. When I use in the the Friend Sub Test the following line instead: 'Dim str_rootdirectory As String = Directory.GetCurrentDirectory() ' "C:\TEMP" it works. Whats the difference?

My VB.NET code:

Public Class Form1
    Public Sub recur_getdirectories(ByVal di As DirectoryInfo)
        For Each directory As DirectoryInfo In di.GetDirectories()
            'get each directory and call the module main to get the security info and write to json
            Call Module1.Main(directory.FullName)
            recur_getdirectories(directory)
        Next
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim rootDirectory As String = TextBox1.Text.Trim()
        Dim di As New DirectoryInfo(rootDirectory)

        'get directories recursively and work with each of them
        recur_getdirectories(di)
    End Sub
End Class

Public Module RecursiveEnumerableExtensions
    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

Public Module TestClass
    Function GetFileSystemAccessRule(d As DirectoryInfo) As IEnumerable(Of FileSystemAccessRule)
        Dim ds As DirectorySecurity = d.GetAccessControl()
        Dim arrRules As AuthorizationRuleCollection = ds.GetAccessRules(True, True, GetType(Security.Principal.NTAccount))

        For Each authorizationRule As FileSystemAccessRule In arrRules
            Dim strAclIdentityReference As String = authorizationRule.IdentityReference.ToString()
            Dim strInheritanceFlags As String = authorizationRule.InheritanceFlags.ToString()
            Dim strAccessControlType As String = authorizationRule.AccessControlType.ToString()
            Dim strFileSystemRights As String = authorizationRule.FileSystemRights.ToString()
            Dim strIsInherited As String = authorizationRule.IsInherited.ToString()
        Next

        ' This function should return the following values, because they should be mentoined in the JSON:
        ' IdentityReference = strAclIdentityReference
        ' InheritanceFlags = strInheritanceFlags
        ' AccessControlType = strAccessControlType
        ' FileSystemRights = strFileSystemRights
        ' IsInherited = strIsInherited

        Return ds.GetAccessRules(True, True, GetType(System.Security.Principal.NTAccount)).Cast(Of FileSystemAccessRule)()
    End Function

    Friend Sub Test(ByVal curDirectory As String)
        'Dim str_rootdirectory As String = Directory.GetCurrentDirectory() ' "C:\TEMP"
        Dim str_rootdirectory As String = curDirectory

        Dim di As DirectoryInfo = New DirectoryInfo(str_rootdirectory)

        Dim directoryQuery = RecursiveEnumerableExtensions.Traverse(di, Function(d) d.GetDirectories())

        Dim list = directoryQuery.Select(
            Function(d) New With {
                .directory = d.FullName,
                .permissions = {
                    GetFileSystemAccessRule(d).ToDictionary(Function(a) a.IdentityReference.ToString(), Function(a) a.FileSystemRights.ToString())
                }
            }
        )
        Dim json = JsonConvert.SerializeObject(list, Formatting.Indented)
        File.WriteAllText("ABCD.json", json)
    End Sub
End Module

Public Module Module1
    Public Sub Main(ByVal curDirectory As String)
        Console.WriteLine("Environment version: " & Environment.Version.ToString())
        Console.WriteLine("Json.NET version: " & GetType(JsonSerializer).Assembly.FullName)
        Console.WriteLine("")
        Try
            TestClass.Test(curDirectory)

        Catch ex As Exception
            Console.WriteLine("Unhandled exception: ")
            Console.WriteLine(ex)
            Throw
        End Try
    End Sub
End Module

My example folder structure:

Folder: "C:\Temp"

Permissions: SecurityGroup-A has Fullcontrol, SecurityGroup-B has Modify permission

Folder: "C:\Temp\Folder_A"

Permissions: SecurityGroup-C has Fullcontrol

But this is only an example of two folders. In real, it will run over several hundered folders with sub-folders. Accordingly the JSON will extend.

My json output expectation:

[{
        "directory": "C:\\TEMP",
        "permissions": [{
                "IdentityReference": "CONTOSO\\SecurityGroup-A",
                "AccessControlType": "Allow",
                "FileSystemRights": "FullControl",
                "IsInherited": "TRUE"
            }, {
                "IdentityReference": "CONTOSO\\SecurityGroup-B",
                "AccessControlType": "Allow",
                "FileSystemRights": "Modify",
                "IsInherited": "False"
            }
        ]
    }, {
        "directory": "C:\\TEMP\\Folder_A",
        "permissions": [{
                "IdentityReference": "CONTOSO\\SecurityGroup-C",
                "AccessControlType": "Allow",
                "FileSystemRights": "Full Control",
                "IsInherited": "False"
            }
        ]
    }
]
Baku Bakar
  • 442
  • 2
  • 8
  • 20
  • Your JSON contains duplicate property names -- `"RandomValue01"` appears twice in the same `permissions` object. Is that a typo in your question or do you really need that? – dbc Dec 25 '20 at 20:59
  • The most recent [JSON RFC](https://www.rfc-editor.org/rfc/rfc8259.html#section-4) states *When the names within an object are not unique, the behavior of software that receives such an object is unpredictable.* so I don't recommend allowing duplicate names within a single JSON object. – dbc Dec 25 '20 at 21:09
  • Thank you for the information, but the json output with randomvalue are just an example, to understand my code better. In real the values will be unique. My question based more on my VB.NET code, which will grow up with each new RandomValue - i want to prevent this, but how? – Baku Bakar Dec 25 '20 at 21:13
  • OK, in that case you may define the `permissions` property as a `List(Of Dictionary(Of String, String))`. Json.NET (and other JSON serializers like `System.Text.Json`) will serialize a dictionary as a JSON object with dynamic keys. Demo here: https://dotnetfiddle.net/OeonWQ. Does that answer your question? – dbc Dec 25 '20 at 21:14
  • thank you, but i think I was not able the place my request correctly. lets continue to my question based on your code. Imagine in your code you have a new sub. inside the sub there is a code, which will loop recursively trough your windows filesystem and get `a) all directorys in your windows explorer` and `b) read the security permissions of each directory`. The data you will receive are more than thousend. But you want to have the output in JSON. How would you implement this in code, without using the `hardcoded values` in `line 31/38 for directory` and `line 33/34/40 for RandomValues`? – Baku Bakar Dec 25 '20 at 21:36
  • Then might you please [edit] your question to and add in a [mcve]? It's not clear where you are having problems. Is your problem how to loop recursively through the filesystem? Or how to get the 1000 values for each directory? Or what? – dbc Dec 25 '20 at 21:41
  • I will edit my question. But my issue is not how to loop recursively trough the filesystem or get the 1000 values. My issue is, when I do have this result - which i will receive form a for each loop - how can I fill in these results to JSON, without duplicating my code with permission1, permission 2 etc. same for New Dictionary, which is appearing 3 times in my code. – Baku Bakar Dec 25 '20 at 21:45
  • You can probably do it with a LINQ [`ToDictionary()`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.todictionary?view=net-5.0) expression but I need to understand your inputs and where you are stuck to make a suggestion how to do it. – dbc Dec 25 '20 at 21:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226427/discussion-between-baku-bakar-and-dbc). – Baku Bakar Dec 25 '20 at 22:28
  • Just let me know, if I was clear enough. I tried with ToDictionary() but without any success. Where do I have to use it? Do you have an example? Thank you. – Baku Bakar Dec 26 '20 at 01:16
  • It's not clear where your problem is exactly because you don't share a [mcve], e.g. you don't show how `arrRules` is determined. But is this what you want? https://dotnetfiddle.net/zTd3tW The code here enumerates through a directory hierarchy and, for each `DirectoryInfo`, enumerates through its `FileSystemAccessRule` objects and projects the result to a dictionary. note that the code won't run on https://dotnetfiddle.net/ because of security permission issues. – dbc Dec 26 '20 at 03:36
  • Read/watch some tutorials and learn the basics. There also exist `Function`s! – Christoph Dec 26 '20 at 10:42
  • @dbc everything works great, thank you! only one quesiton: when I replace `Directory.GetCurrentDirectory()` on `line 57` with for example with "C:\Temp" it gives me later on `line 63` that `system.argumentexception an element with the same key already exists`. Where should be the input of the specific directory before starting with the work? – Baku Bakar Dec 26 '20 at 12:37
  • Please share a [mcve]. But if you have two `FileSystemAccessRule` objects for the same directory with the same name (i.e. `a.IdentityReference.ToString()`) then this approach will not work because you will be trying to create a dictionary (and subsequently a JSON object) with duplicate keys. As mentioned above this is not a good idea -- and you wrote that you did not need to do it. – dbc Dec 26 '20 at 15:37
  • @dbc i did update my question, with the minimal reproducible example. Please take care about my comments in the code. I hope now it's more clear, what and where the issue is. Regarding your comment: my code will run on a filesystem, on which a existing filestructure is applied. Due do windows file system is not allowing to greate for dictionary duplicated foldernames on same direcotry, there shoudln't be a issue. Please let me know, if there are any questions. Thank you. – Baku Bakar Dec 26 '20 at 23:17
  • Now that you have replaced your variable property names with fixed property names there's no reason to use a dictionary. Is this what you want? https://dotnetfiddle.net/Ie05UT – dbc Dec 27 '20 at 02:53

1 Answers1

1

Your current JSON uses static property names for the [*].permissions[*] objects so there is no need to try to convert a list of them into a dictionary with variable key names via ToDictionary():

' This is not needed
.permissions = {
    GetFileSystemAccessRule(d).ToDictionary(Function(a) a.IdentityReference.ToString(), Function(a) a.FileSystemRights.ToString())
}

Instead, convert each FileSystemAccessRule into some appropriate DTO for serialization. An anonymous type object works nicely for this purpose:

Public Module DirectoryExtensions
    Function GetFileSystemAccessRules(d As DirectoryInfo) As IEnumerable(Of FileSystemAccessRule)
        Dim ds As DirectorySecurity = d.GetAccessControl()
        Dim arrRules As AuthorizationRuleCollection = ds.GetAccessRules(True, True, GetType(Security.Principal.NTAccount))
        Return arrRules.Cast(Of FileSystemAccessRule)()
    End Function
    
    Public Function SerializeFileAccessRules(ByVal curDirectory As String, Optional ByVal formatting As Formatting = Formatting.Indented)
        Dim di As DirectoryInfo = New DirectoryInfo(curDirectory)

        Dim directoryQuery = RecursiveEnumerableExtensions.Traverse(di, Function(d) d.GetDirectories())

        Dim list = directoryQuery.Select(
            Function(d) New With {
                .directory = d.FullName,
                .permissions = GetFileSystemAccessRules(d).Select(
                    Function(a) New With { 
                        .IdentityReference = a.IdentityReference.ToString(),
                        .AccessControlType = a.AccessControlType.ToString(),
                        .FileSystemRights = a.FileSystemRights.ToString(),
                        .IsInherited = a.IsInherited.ToString()
                    }
                )
            }
        )
        Return JsonConvert.SerializeObject(list, formatting)    
    End Function
End Module

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

Demo fiddle here (which unfortunately does not work on https://dotnetfiddle.net because of security restrictions on client code but should be runnable in full trust).

dbc
  • 104,963
  • 20
  • 228
  • 340