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.