I was recently faced with the task of projecting Active Directory containers onto domain types. The subset of Active Directory in question (illustrated below) holds courses, such as English, each subdivided into one or more classes (English A, B, C). Students are then added to each class. Finally, the purpose of my F# code is to construct a collection of courses containing classes containing students for later processing.
DC=Acme, DC=local OU=Acme OU=Administration and Access OU=Groups OU=Courses OU=English objectGUID (attribute) name (attribute) CN=English A objectGuid (attribute) name (attribute) member (attribute pointing to Students) CN=English B CN=English C OU=Math OU=Drama ... OU=User Accounts OU=Students CN=John Doe objectGUID (attribute) name (attribute) mail (attribute) CN=Jane Doe ...
First, I need to define the F# types to project Active Directory objects onto. These types define an aggregate with Course as its root and child types matching the hierarchy of containers in Active Directory:
open System open System.Collections open System.DirectoryServices type Student = { Id: Guid Name: string Mail: string } type Class = { Id: Guid Name: string Students: Student list } type Course = { Id: Guid Name: string Classes: Class list }
Next, I need to project the hierarchical data of Active Directory onto the three newly defined F# types. Instead of creating a repository representing Active Directory, I went with a simpler query type (as in the Command/Query pattern):
type CoursesQuery(username: string, password: string, server: string) = let parseStudents (e: DirectoryEntry) = // accessing members through e.Properties.["member"] only yields // CommonName and path to object in string format. Problem is // that CommonName isn't unique. Instead we follow the path // and read properties of the Student object directly. let members = e.Invoke("members", null) [ for dn in (members :?> IEnumerable) do use e = new DirectoryEntry(dn) yield { Id = e.Guid Name = e.Properties.["name"].[0] |> string Mail = e.Properties.["mail"].[0] |> string } ] let parseClass (klass: DirectoryEntry) = { Id = klass.Guid Name = klass.Properties.["name"].[0] |> string Students = parseStudents klass } let parseCourse (sr: SearchResult) = { Id = Guid(sr.Properties.["objectGUID"].[0] :?> byte[]) Name = sr.Properties.["name"].[0] |> string Classes = sr.GetDirectoryEntry().Children |> Seq.cast |> Seq.map parseClass |> Seq.toList } member __.Execute() = let coursesDirectory = new DirectoryEntry( Username = username, Password = password, Path = sprintf "LDAP://%s/OU=Courses,OU=Groups,OU=Administration and Access,OU=Acme,DC=Acme,DC=local" server) use coursesSearcher = new DirectorySearcher( SearchRoot = coursesDirectory, Filter = "(ou=*)") coursesSearcher.FindAll() |> Seq.cast |> Seq.map parseCourse |> Seq.toList
Calling Execute on CoursesQuery returns a list of Course types.
Depending on desired runtime characteristics, this sort of mapping may be too slow. The way the code traverses the containers results in significant request/reply network chatter. If performance is paramount, one should construct one or multiple LDAP queries. Traversing containers like above vs. using LDAP queries is like querying an SQL database with many small select queries versus using joins and doesn't scale.