Larry Steinle

March 18, 2011

AdDataReader: Providing Controlled Access to AD Values

Filed under: Active Directory,VS.Net — Larry Steinle @ 8:18 pm
Tags: , , ,

Today we will implement the DbDataReader class which enforces a contract that defines how to make data available to applications.

This is the sixth post of an eight part series about the Active Directory Data Access Layer. As each post builds on the previous it may be helpful to review older posts prior to reading this one. If you would like to download a working copy of the AD DAL please refer to the download on the Code Share page.

AdDataReader Class Diagram

The AdDataReader class has several properties that assist with data conversion. Unfortunately with Active Directory attributes there are some attribute types that must continue to be manually converted. Take for example the GetByte method. There are a couple of different ways to convert the value to byte. Currently this class hasn’t been configured to use the schema to correctly decode the values.

AdDataReader uses the AdConvert class to help with the data conversion. AdConvert can be used to convert the object value as necessary. Or a custom routine can be constructed to convert the object value from the AdDataReader when necessary.

The AdSchemaBuilder class assists with creating the schema DataTable for the AdDataReader. AdSchemaBuilder is also used by the AdCommand to identify when an attribute is a multivalued attribute or a single-valued attribute.

AdDataReader uses both AdCommand and AdConnection to manage retrieving the data from Active Directory. When batched commands are executed the NextResult method can be used to advance to the next result set. The Read method continues to provide access to the next available record returning false when there are no more records to read.

AdDataReader Class Diagram

AdDataReader Class Diagram

With the AdConnection, AdCommand and AdDataReader classes we are able to use standard data access interfaces to manage Active Directory.

AdConvert

Namespace Data.ActiveDirectory
  ''' <summary>
  ''' Converts an ADSI base data type to another base data type.
  ''' </summary>
  ''' <remarks></remarks>
  Public Class AdConvert
    ''' <summary>
    ''' Gets the value of the specified column as a Boolean.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Boolean.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToBoolean(ByVal value As Object) As Boolean
      If IsNumeric(value) Then
        If value.ToString.IndexOf(".") >= 0 Then
          Return Convert.ToBoolean(Convert.ToDecimal(value))
        Else
          Return Convert.ToBoolean(Convert.ToInt64(value))
        End If
      Else
        Return Convert.ToBoolean(value)
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a byte.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Byte.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToByte(ByVal value As Object) As Byte
      Return Convert.ToByte(value)
    End Function

    ''' <summary>
    ''' Converts a character string to a byte array (e.g. "abcdefg" to [97,98,99,100,101,102,103]).
    ''' </summary>
    ''' <param name="value">The value to convert to a Byte array.</param>
    ''' <returns>Byte Array</returns>
    ''' <remarks></remarks>
    Public Shared Function ToByteArray(ByVal value As Object) As Byte()
      Dim s As Char() = value.ToString.ToCharArray

      Dim b(s.Length - 1) As Byte
      For i As Integer = 0 To s.Length - 1
        b(i) = System.Convert.ToByte(s(i))
      Next

      Return b
    End Function

    ''' <summary>
    ''' Converts a hexadecimal value to a byte array.
    ''' </summary>
    ''' <param name="hexString">The hex string value to convert a Byte array.</param>
    ''' <returns>Hex byte array.</returns>
    ''' <remarks></remarks>
    Public Shared Function HexToByteArray(ByVal hexString As String) As Byte()
      If hexString.Length Mod 2 <> 0 Then
        Throw New ApplicationException("Hex string must be multiple of 2 in length")
      End If

      Dim byteCount As Integer = System.Convert.ToInt32(hexString.Length / 2)
      Dim byteValues(byteCount) As Byte
      For i As Integer = 0 To byteCount - 1
        byteValues(i) = System.Convert.ToByte(hexString.Substring(i * 2, 2), 16)
      Next

      Return byteValues
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a single character.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Char.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToChar(ByVal value As Object) As Char
      If value IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(value.ToString) Then value = value.ToString.Trim.Substring(0, 1)
      Return Convert.ToChar(value)
    End Function

    ''' <summary>
    ''' Reads a stream of characters from the specified column,
    ''' starting at location indicated by dataIndex, into the buffer,
    ''' starting at the location indicated by bufferIndex.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to an array of Char.
    ''' </param>
    ''' <param name="dataOffset">
    ''' The index within the row from which to begin the read operation.
    ''' </param>
    ''' <param name="buffer">
    ''' The buffer into which to copy the data.
    ''' </param>
    ''' <param name="bufferOffset">
    ''' The index with the buffer to which the data will be copied.
    ''' </param>
    ''' <param name="length">
    ''' The maximum number of characters to read.
    ''' </param>
    ''' <returns>
    ''' The actual number of characters read.
    ''' </returns>
    ''' <remarks></remarks>
    Public Shared Function ToChars(ByVal value As Object, ByVal dataOffset As Long, ByVal buffer() As Char, ByVal bufferOffset As Integer, ByVal length As Integer) As Long
      If length < 0 Then Throw New IndexOutOfRangeException("Length is out of range.")
      If buffer IsNot Nothing Then
        If bufferOffset < 0 OrElse bufferOffset >= buffer.Length Then
          Throw New IndexOutOfRangeException("bufferOffset is out of range.")
        ElseIf (length + bufferOffset) >= buffer.Length Then
          Throw New IndexOutOfRangeException("Length + bufferOffset is out of range.")
        End If
      End If

      Dim holdCharArray() As Char = value.ToString.ToCharArray
      Dim charactersRead As Long

      If dataOffset >= holdCharArray.Length Then
        'Begin reading outside bounds of char array
        charactersRead = 0
      ElseIf dataOffset + length >= holdCharArray.Length Then
        'Stop reading outside bounds of char array
        charactersRead = holdCharArray.Length - dataOffset - 1
      Else
        'Reading occurs within bounds of char array
        charactersRead = length
      End If

      If charactersRead > 0 Then
        Array.Copy(holdCharArray, dataOffset, buffer, bufferOffset, charactersRead)
      End If

      Return charactersRead
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a DateTime object.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to DateTime.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToDateTime(ByVal value As Object) As Date
      If TypeOf value Is DateTime Then
        Return CType(value, DateTime)
      ElseIf IsNumeric(value) Then
        Return DateTime.FromFileTime(Convert.ToInt64(value))
      Else
        Try
          Return Convert.ToDateTime(value)
        Catch ex As Exception
          Throw New InvalidCastException(String.Format("Cannot convert {0} to DateTime", value))
        End Try
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a Decimal object.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Decimal.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToDecimal(ByVal value As Object) As Decimal
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToDecimal(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to decimal", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a double-precision floating point number.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Double.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToDouble(ByVal value As Object) As Double
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToDouble(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to double", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a single-precision floating point number.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Single.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToFloat(ByVal value As Object) As Single
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToSingle(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to single", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a globally-unique identifier (GUID).
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Guid.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToGuid(ByVal value As Object) As System.Guid
      Dim strValue As String = Nothing
      Try
        strValue = ToString(value)
        Return Guid.Parse(strValue)
      Catch ex As Exception
        If String.IsNullOrWhiteSpace(strValue) Then
          Throw New InvalidCastException(String.Format("Cannot convert {0} to Guid", value.ToString))
        Else
          Throw New InvalidCastException(String.Format("Cannot convert {0} to Guid", strValue))
        End If
      End Try
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 16-bit signed integer.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Short.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToInt16(ByVal value As Object) As Short
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToInt16(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to Int16", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 32-bit signed integer.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Integer.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToInt32(ByVal value As Object) As Integer
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToInt32(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to Int32", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 64-bit signed integer.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to Long.
    ''' </param>
    ''' <remarks></remarks>
    Public Shared Function ToInt64(ByVal value As Object) As Long
      If value Is Nothing OrElse IsNumeric(value) Then
        Return Convert.ToInt64(value)
      Else
        Throw New InvalidCastException(String.Format("Cannot convert {0} to Int64", value))
      End If
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as an instance of String.
    ''' </summary>
    ''' <param name="value">
    ''' The value to convert to String.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Shared Function ToString(ByVal value As Object) As String
      Try
        If value Is Nothing Then
          Return String.Empty
        ElseIf TypeOf value Is System.Byte Then
          Dim _ByteArray() As System.Byte = CType(value, System.Byte())
          Return System.BitConverter.ToString(_ByteArray, 0, _ByteArray.Length)
        ElseIf value.GetType.FullName = "System.__ComObject" Then
          'ComObject type conversion not supported.
          Throw New AdException("Cannot convert type __ComObject")
        Else
          Return Convert.ToString(value)
        End If
      Catch ex As Exception
        Throw New InvalidCastException(String.Format("Cannot convert {0} to string", value), ex)
      End Try
    End Function
  End Class
End Namespace

AdSchemaBuilder

Namespace Data.ActiveDirectory
  ''' <summary>
  ''' Constructs the schema for the specified class name.
  ''' </summary>
  ''' <remarks></remarks>
  Public Class AdSchemaBuilder
#Region "Constructor Section"
    Private _Connection As AdConnection
    Public Sub New(ByVal connection As AdConnection)
      _Connection = connection
    End Sub
#End Region

#Region "Property Section"
    Private ReadOnly Property DomainController As String
      Get
        Dim connectionBuilder As New AdConnectionStringBuilder(_Connection.ConnectionString)
        If String.IsNullOrWhiteSpace(connectionBuilder.DomainController) Then
          Dim domainName As String = connectionBuilder.DomainName
          If String.IsNullOrWhiteSpace(domainName) Then
            domainName = connectionBuilder.DefaultDomainName
          End If
          Return connectionBuilder.GetDefaultDomainController(domainName)
        Else
          Return connectionBuilder.DomainController
        End If
      End Get
    End Property
#End Region

#Region "Exposed Behavior"
    ''' <summary>
    ''' Get the list of multivalued mandatory and optional properties for the specified className from the schema.
    ''' </summary>
    ''' <param name="className">
    ''' The className to query.
    ''' </param>
    ''' <returns>
    ''' Returns a string array of mandatory and optional property names for the specified className.
    ''' </returns>
    ''' <remarks></remarks>
    Public Shared Function GetMultivaluedNames(ByVal className As String) As String()
      Dim currentSchema As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema = Nothing
      Dim schemaClass As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass = Nothing
      Dim propertyNames() As String

      Try
        currentSchema = System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema.GetCurrentSchema
        schemaClass = currentSchema.FindClass(className)

        Dim mandatoryProperties As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaPropertyCollection = schemaClass.MandatoryProperties
        Dim optionalProperties As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaPropertyCollection = schemaClass.OptionalProperties

        'Construct Property Array to Store Mandatory and Optional Properties
        ReDim propertyNames(mandatoryProperties.Count + optionalProperties.Count - 1)
        Dim propertyIndex As Integer = 0

        'Get Mandatory Properties
        For Each adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty In mandatoryProperties
          If Not adProperty.IsSingleValued Then
            propertyNames(propertyIndex) = adProperty.Name
            propertyIndex += 1
          End If
        Next

        'Get Optional Properties
        For Each adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty In optionalProperties
          If Not adProperty.IsSingleValued Then
            propertyNames(propertyIndex) = adProperty.Name
            propertyIndex += 1
          End If
        Next
      Catch ex As Exception
        Throw New AdException("An error occurred while getting the property names from the schema for class " & className & ".", ex)
      Finally
        If schemaClass IsNot Nothing Then schemaClass.Dispose()
        If currentSchema IsNot Nothing Then currentSchema.Dispose()
        schemaClass = Nothing
        currentSchema = Nothing
      End Try

      Return propertyNames
    End Function

    ''' <summary>
    ''' Get the list of mandatory and optional properties for the specified className from the schema.
    ''' </summary>
    ''' <param name="className">
    ''' The className to query.
    ''' </param>
    ''' <returns>
    ''' Returns a string array of mandatory and optional property names for the specified className.
    ''' </returns>
    ''' <remarks></remarks>
    Public Shared Function GetPropertyNames(ByVal className As String) As String()
      Dim currentSchema As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema = Nothing
      Dim schemaClass As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass = Nothing
      Dim propertyNames() As String

      Try
        currentSchema = System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema.GetCurrentSchema
        schemaClass = currentSchema.FindClass(className)

        Dim mandatoryProperties As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaPropertyCollection = schemaClass.MandatoryProperties
        Dim optionalProperties As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaPropertyCollection = schemaClass.OptionalProperties

        'Construct Property Array to Store Mandatory and Optional Properties
        ReDim propertyNames(mandatoryProperties.Count + optionalProperties.Count - 1)
        Dim propertyIndex As Integer = 0

        'Get Mandatory Properties
        For Each adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty In mandatoryProperties
          propertyNames(propertyIndex) = adProperty.Name
          propertyIndex += 1
        Next

        'Get Optional Properties
        For Each adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty In optionalProperties
          propertyNames(propertyIndex) = adProperty.Name
          propertyIndex += 1
        Next
      Catch ex As Exception
        Throw New AdException("An error occurred while getting the property names from the schema for class " & className & ".", ex)
      Finally
        If schemaClass IsNot Nothing Then schemaClass.Dispose()
        If currentSchema IsNot Nothing Then currentSchema.Dispose()
        schemaClass = Nothing
        currentSchema = Nothing
      End Try

      Return propertyNames
    End Function

    ''' <summary>
    ''' Gets the schema for the specified className.
    ''' </summary>
    ''' <param name="className">
    ''' The name of the class to get the schema.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Build(ByVal className As String) As System.Data.DataTable
      Return Build(className, Nothing)
    End Function

    ''' <summary>
    ''' Gets the schema for the specified className.
    ''' </summary>
    ''' <param name="className">
    ''' The name of the class to get the schema.
    ''' </param>
    ''' <param name="propertyNames">
    ''' A list of attributes to add to the schema.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Build(ByVal className As String, ByVal ParamArray propertyNames() As String) As System.Data.DataTable
      If propertyNames Is Nothing OrElse propertyNames.Length = 0 Then
        propertyNames = GetPropertyNames(className)
      End If

      Dim schemaTable As New DataTable("SchemaTable")
      schemaTable.Locale = System.Globalization.CultureInfo.InvariantCulture

      With schemaTable.Columns
        .Add("ColumnName", GetType(String))     'Column Name - attributeName
        .Add("ColumnOrdinal", GetType(Integer))   'Column Index
        .Add("ColumnSize", GetType(Integer))    'Column Size
        .Add("NumericPrecision", GetType(Short))  '-1 if not number.
        .Add("NumericScale", GetType(Short))    '-1 if not number. Precesion right of the decimal point.
        .Add("DataType", GetType(Type))       '.Net Data Type
        .Add("ProviderType", GetType(Type))     '-1 for COM Object else same as data type
        .Add("IsLong", GetType(Boolean))
        .Add("AllowDBNull", GetType(Boolean))
        .Add("IsReadOnly", GetType(Boolean))
        .Add("IsUnique", GetType(Boolean))
        .Add("IsKey", GetType(Boolean))
        .Add("IsAutoIncrement", GetType(Boolean))   'Always false
        .Add("IsSingleValued", GetType(Boolean))
        .Add("IsMultiValued", GetType(Boolean))
        .Add("IsTupleIndexed", GetType(Boolean))
        .Add("IsMandatory", GetType(Boolean))
        .Add("IsOptional", GetType(Boolean))
        .Add("Link", GetType(String))
        .Add("LinkID", GetType(Integer))
        .Add("Oid", GetType(String))
        .Add("RangeLower", GetType(Integer))
        .Add("RangeUpper", GetType(Integer))
        .Add("BaseSchemaName", GetType(String))   'empty string
        .Add("BaseCatalogName", GetType(String))  'domain controller
        .Add("BaseTableName", GetType(String))    'objectClass
        .Add("BaseColumnName", GetType(String))   'attributeName
      End With

      Dim domainController As String = Me.DomainController
      Dim currentSchema As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema = Nothing
      Dim schemaClass As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass = Nothing

      Try
        currentSchema = System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema.GetCurrentSchema
        schemaClass = currentSchema.FindClass(className)

        AddPropertiesToSchema(schemaTable, schemaClass.CommonName, True, schemaClass.MandatoryProperties, propertyNames)
        AddPropertiesToSchema(schemaTable, schemaClass.CommonName, False, schemaClass.OptionalProperties, propertyNames)
      Catch ex As Exception
        Throw New AdException("An error occurred while getting the schema for class " & className & ".", ex)
      Finally
        If schemaClass IsNot Nothing Then schemaClass.Dispose()
        If currentSchema IsNot Nothing Then currentSchema.Dispose()
        schemaClass = Nothing
        currentSchema = Nothing
      End Try

      Return schemaTable
    End Function

    ''' <summary>
    ''' Appends the schema for the specified className to the provided schema.
    ''' </summary>
    ''' <param name="className">
    ''' The name of the class to get the schema.
    ''' </param>
    ''' <param name="schema">
    ''' The schema to append the classNames results.
    ''' </param>
    ''' <returns>
    ''' The combined schema results.
    ''' </returns>
    ''' <remarks></remarks>
    Public Function AppendSchema(ByVal className As String, ByVal schema As System.Data.DataTable) As System.Data.DataTable
      Return AppendSchema(className, schema, Nothing)
    End Function

    ''' <summary>
    ''' Appends the schema for the specified className to the provided schema.
    ''' </summary>
    ''' <param name="className">
    ''' The name of the class to get the schema.
    ''' </param>
    ''' <param name="schema">
    ''' The schema to append the classNames results.
    ''' </param>
    ''' <param name="propertyNames">
    ''' A list of attributes to add to the schema.
    ''' </param>
    ''' <returns>
    ''' The combined schema results.
    ''' </returns>
    ''' <remarks></remarks>
    Public Function AppendSchema(ByVal className As String, ByVal schema As System.Data.DataTable, ByVal ParamArray propertyNames() As String) As System.Data.DataTable
      Dim appendTable As System.Data.DataTable = Build(className, propertyNames)

      If schema Is Nothing Then
        schema = appendTable
      Else
        'Verify There is a Name
        If String.IsNullOrWhiteSpace(schema.TableName) Then
          schema.TableName = appendTable.TableName
        End If

        'Verify Column Names Match
        For Each appendCol As System.Data.DataColumn In appendTable.Columns
          If Not schema.Columns.Contains(appendCol.ColumnName) Then
            schema.Columns.Add(appendCol.ColumnName)
          End If
        Next

        'Copy rows to schema if they do not already exist in the schema
        For Each appendRow As System.Data.DataRow In appendTable.Rows
          Dim dataFound As Boolean = False
          For Each schemaRow As System.Data.DataRow In schema.Rows
            If String.Compare(schemaRow.Item("ColumnName").ToString, appendRow.Item("ColumnName").ToString, True) = 0 Then
              dataFound = True
              Exit For
            End If
          Next

          If Not dataFound Then schema.Rows.Add(appendRow)
        Next
      End If

      Return schema
    End Function
#End Region

#Region "MetaData Helpers"
    Private Sub AddPropertiesToSchema(ByVal schema As DataTable, ByVal classCommonName As String, ByVal isMandatory As Boolean, ByVal properties As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaPropertyCollection, ByVal ParamArray propertyNames() As String)
      For Each adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty In properties
        For Each propertyName As String In propertyNames
          If String.Compare(adProperty.Name, propertyName, True) = 0 Then
            Dim schemaRow As System.Data.DataRow = schema.NewRow
            Dim adSyntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax = adProperty.Syntax

            With adProperty
              schemaRow.Item("IsMandatory") = isMandatory
              schemaRow.Item("IsOptional") = (Not isMandatory)
              schemaRow.Item("ColumnName") = .Name
              schemaRow.Item("ColumnOrdinal") = schema.Rows.Count
              schemaRow.Item("ColumnSize") = GetColumnSize(adSyntax)
              schemaRow.Item("NumericPrecision") = GetNumericPrecision(adSyntax)
              schemaRow.Item("NumericScale") = GetNumericScale(adSyntax)
              schemaRow.Item("DataType") = GetDataType(adSyntax)
              schemaRow.Item("ProviderType") = GetProviderType(adSyntax)
              schemaRow.Item("IsLong") = GetIsLong(adSyntax)
              schemaRow.Item("AllowDBNull") = GetAllowDBNull(adProperty)
              schemaRow.Item("IsReadOnly") = False
              schemaRow.Item("IsUnique") = GetIsUnique(propertyName)
              schemaRow.Item("IsKey") = GetIsKey(adProperty)
              schemaRow.Item("IsAutoIncrement") = False
              schemaRow.Item("IsSingleValued") = .IsSingleValued
              schemaRow.Item("IsMultiValued") = Not .IsSingleValued
              schemaRow.Item("IsTupleIndexed") = .IsTupleIndexed
              If .Link IsNot Nothing AndAlso .Link.Name IsNot Nothing Then schemaRow.Item("Link") = .Link.Name
              If .LinkId IsNot Nothing Then schemaRow.Item("LinkID") = .LinkId
              schemaRow.Item("Oid") = .Oid
              If .RangeLower IsNot Nothing Then schemaRow.Item("RangeLower") = .RangeLower
              If .RangeUpper IsNot Nothing Then schemaRow.Item("RangeUpper") = .RangeUpper
              schemaRow.Item("BaseSchemaName") = String.Empty
              schemaRow.Item("BaseCatalogName") = DomainController
              schemaRow.Item("BaseTableName") = classCommonName
              schemaRow.Item("BaseColumnName") = .CommonName
            End With

            schema.Rows.Add(schemaRow)
          End If
        Next
      Next
    End Sub

    ''' <summary>
    ''' Gets a boolean value that indicates if the specified attribute is unique.
    ''' </summary>
    ''' <param name="attributeName"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' A return value of false does not mean that the attribute is not unique.
    ''' There is no way to identify if an attribute is unique. Therefore this
    ''' routine checks the attributeName against a known list of unique names.
    ''' </remarks>
    Private Function GetIsUnique(ByVal attributeName As String) As Boolean
      Dim uniqueNames() As String = {"sAMAccountName", "userPrincipalName", "distinguishedName", "objectGuid", "objectSid", "sIDHistory"}

      For Each uniqueName As String In uniqueNames
        If String.Compare(uniqueName, attributeName, True) = 0 Then
          Return True
        End If
      Next

      Return False
    End Function

    ''' <summary>
    ''' Gets a value that indicates if the attribute can contain a null value.
    ''' </summary>
    ''' <param name="adProperty">
    ''' The attribute to test for nullability.
    ''' </param>
    ''' <returns>
    ''' Returns true if the attribute can store an empty value, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Private Function GetAllowDBNull(ByVal adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty) As Boolean
      Return (Not adProperty.IsIndexed AndAlso Not adProperty.IsIndexedOverContainer)
    End Function

    ''' <summary>
    ''' Gets a value that indicates if the attribute is a primary key attribute.
    ''' </summary>
    ''' <param name="adProperty">
    ''' The attribute to test for primary key enforcement.
    ''' </param>
    ''' <returns>
    ''' Returns true if the key is a known primary key, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Private Function GetIsKey(ByVal adProperty As System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty) As Boolean
      Return ((adProperty.IsIndexed OrElse adProperty.IsIndexedOverContainer) AndAlso GetIsUnique(adProperty.Name))
    End Function

    ''' <summary>
    ''' Gets the column size for the specified syntax.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function GetColumnSize(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Integer
      Dim dataType As Type = GetDataType(syntax)
      If dataType = GetType(String) Then
        'The maximum number of characters in a string
        Return Integer.MaxValue
      ElseIf dataType = GetType(Int32) Then
        'The number of bytes is the length
        Return 32
      ElseIf dataType = GetType(Int64) Then
        'The number of bytes is the length
        Return 64
      Else
        Return 0
      End If
    End Function

    ''' <summary>
    ''' Gets the numeric precision for the specified syntax.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns>
    ''' Returns the numeric precision value or DBNull if precision does not apply.
    ''' </returns>
    ''' <remarks>
    ''' Precision is the number of placements to the left of the decimal point.
    ''' </remarks>
    Private Function GetNumericPrecision(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Integer
      Dim dataType As Type = GetDataType(syntax)
      If dataType = GetType(Int32) Then
        Return Int32.MaxValue.ToString.Length
      ElseIf dataType = GetType(Int64) Then
        Return Int64.MaxValue.ToString.Length
      Else
        Return -1
      End If
    End Function

    ''' <summary>
    ''' Gets the numeric scale for the specified syntax.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns>
    ''' Returns the numeric scale value or DBNull if scale does not apply.
    ''' </returns>
    ''' <remarks>
    ''' Scale is the number of placements to the right of the decimal point.
    ''' </remarks>
    Private Function GetNumericScale(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Integer
      Dim dataType As Type = GetDataType(syntax)
      If dataType = GetType(Int32) Then
        Return 0
      ElseIf dataType = GetType(Int64) Then
        Return 0
      Else
        Return -1
      End If
    End Function

    ''' <summary>
    ''' Gets the dot net equivalent of the provider data type.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function GetDataType(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Type
      Select Case syntax
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.AccessPointDN : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Bool : Return GetType(Boolean)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.CaseExactString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.CaseIgnoreString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DirectoryString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DN : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DNWithBinary : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DNWithString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Enumeration : Return GetType(Int32)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.GeneralizedTime : Return GetType(DateTime)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.IA5String : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Int : Return GetType(Int32)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Int64 : Return GetType(Int64)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.NumericString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.OctetString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Oid : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.ORName : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.PresentationAddress : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.PrintableString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.ReplicaLink : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.SecurityDescriptor : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Sid : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.UtcTime : Return GetType(DateTime)
        Case Else : Return GetType(String)
      End Select
    End Function

    ''' <summary>
    ''' Gets the provider data type.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function GetProviderType(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Type
      Select Case syntax
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.AccessPointDN : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Bool : Return GetType(Boolean)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.CaseExactString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.CaseIgnoreString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DirectoryString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DN : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DNWithBinary : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.DNWithString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Enumeration : Return GetType(Int32)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.GeneralizedTime : Return GetType(DateTime)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.IA5String : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Int : Return GetType(Int32)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Int64 : Return GetType(Int64)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.NumericString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.OctetString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Oid : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.ORName : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.PresentationAddress : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.PrintableString : Return GetType(String)
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.ReplicaLink : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.SecurityDescriptor : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Sid : Return GetType(Byte())
        Case DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.UtcTime : Return GetType(DateTime)
        Case Else : Return GetType(String)
      End Select
    End Function

    ''' <summary>
    ''' Gets a value that indicates if the syntax is a long data type.
    ''' </summary>
    ''' <param name="syntax">
    ''' The property syntax type.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function GetIsLong(ByVal syntax As System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax) As Boolean
      If syntax = DirectoryServices.ActiveDirectory.ActiveDirectorySyntax.Int64 Then Return True
      Return False
    End Function
#End Region
  End Class
End Namespace

AdDataReader

Namespace Data.ActiveDirectory
  ''' <summary>
  ''' Reads a forward-only stream of rows from an Active Directory data source.
  ''' </summary>
  ''' <remarks></remarks>
  Public Class AdDataReader
    Inherits System.Data.Common.DbDataReader

#Region "Definition Section"
    Private _Command As AdCommand
    Private _Connection As AdConnection
    Private _CommandBehavior As System.Data.CommandBehavior
    Private _Searcher As System.DirectoryServices.DirectorySearcher
    Private _CurrentSet As System.DirectoryServices.SearchResultCollection
    Private _CurrentRow As System.DirectoryServices.SearchResult
    Private _CurrentSetIndex As Integer
    Private _CurrentRowIndex As Integer
    Private _RecordsAffected As Integer
    Private _IsClosed As Boolean = False
    Private _RecordSetParser As AdCommandTextParser
    Private _Schema As System.Data.DataTable
    Private _MultiValuedAttributes As System.Collections.Specialized.StringCollection
#End Region

#Region "Constructor Section"
    ''' <summary>
    ''' Instantiates a new AdDataReader instance.
    ''' </summary>
    ''' <param name="command">
    ''' The command class that created the AdDataReader instance.
    ''' </param>
    ''' <param name="behavior">
    ''' Specifies how the AdDataReader should behave when finished reading data.
    ''' </param>
    ''' <remarks>
    ''' To get an AdDataReader you must use AdCommand.ExecuteReader.
    ''' </remarks>
    Friend Sub New(ByVal command As AdCommand, ByVal behavior As System.Data.CommandBehavior)
      _CurrentSetIndex = -1
      _HasRows = False

      _Command = command
      _Connection = DirectCast(command.Connection, AdConnection)
      _CommandBehavior = behavior

      For Each parser As AdCommandTextParser In _Command.InnerCommandSets
        If parser.OrderBy.Count > 1 Then
          _Connection.OnInfoMessage("WARNING! AdDataReader supports sorting on only the first column. Multiple sorts are not supported.")
        End If
      Next

      'Get First Recordset
      NextResult()
    End Sub

    ''' <summary>
    ''' Returns the index to the current recordset.
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly Property DatasetIndex As Integer
      Get
        Return _CurrentSetIndex
      End Get
    End Property
#End Region

#Region "Helper Routines"
    ''' <summary>
    ''' Test condition to see if class supports given CommandBehavior.
    ''' </summary>
    ''' <param name="condition">
    ''' The command behavior to test for support.
    ''' </param>
    ''' <returns>
    ''' Returns true if the specified behavior is supported, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Private Function IsCommandBehavior(ByVal condition As System.Data.CommandBehavior) As Boolean
      Return (condition = (condition And _CommandBehavior))
    End Function

    ''' <summary>
    ''' Throws an InvalidOperationiException when their are no results.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub ThrowErrorOnInvalidReadNext()
      If _Command.InnerCommandSets Is Nothing _
      OrElse _Command.InnerCommandSets.Count = 0 Then
        Throw New InvalidOperationException("Attempted to read an invalid record set.")
      End If
    End Sub

    ''' <summary>
    ''' Throws an InvalidOperationiException when the current row is null.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub ThrowErrorOnInvalidRead()
      If _CurrentRow Is Nothing Then
        Throw New InvalidOperationException("Attempted to read an invalid record.")
      End If
    End Sub

    ''' <summary>
    ''' Get a value that indicates that the field is capable of storing an array of values.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <returns>
    ''' Returns true when the specified field stores an array of values, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Public Function IsFieldMultiValued(ByVal ordinal As Integer) As Boolean
      Return IsFieldMultiValued(GetName(ordinal))
    End Function

    ''' <summary>
    ''' Get a value that indicates that the field is capable of storing an array of values.
    ''' </summary>
    ''' <param name="name">
    ''' The attribute name.
    ''' </param>
    ''' <returns>
    ''' Returns true when the specified field stores an array of values, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Public Function IsFieldMultiValued(ByVal name As String) As Boolean
      'Track all Multi-valued attributes
      If _MultiValuedAttributes Is Nothing Then
        If _Schema Is Nothing Then _Schema = GetSchemaTable()
        _MultiValuedAttributes = New System.Collections.Specialized.StringCollection

        For Each row As DataRow In _Schema.Rows
          If Not row.IsNull("IsMultiValued") _
          AndAlso row.Item("IsMultiValued").ToString = "True" Then
            _MultiValuedAttributes.Add(row.Item("ColumnName").ToString)
          End If
        Next
      End If

      'Identify if the specified field name is a multi-valued attribute
      Dim innerName As String = name.Trim
      For Each attributeName As String In _MultiValuedAttributes
        If String.Compare(innerName, attributeName, True) = 0 Then
          Return True
        End If
      Next
      Return False
    End Function

    ''' <summary>
    ''' Use Range Retrieval to get all the values assigned to a multi-valued attribute.
    ''' </summary>
    ''' <param name="adsPath">
    ''' Reference to current object containing the attribute to use range retrieval to query.
    ''' </param>
    ''' <param name="attributeName">
    ''' The name of the attribute to query.
    ''' </param>
    ''' <remarks>
    ''' Avoid using the routine if at all possible as there will be a negative performance impact.
    ''' Use of this routine requires a secondary DirectorySearcher object and multiple calls to
    ''' get each object's values.
    ''' References: http://msdn.microsoft.com/en-us/library/ms180907(v=vs.80).aspx#Y192
    ''' </remarks>
    Private Function RangeRetrieval(ByVal adsPath As Path, ByVal attributeName As String) As Object()
      Dim searcher As DirectoryServices.DirectorySearcher = Nothing
      Dim objList As New System.Collections.ArrayList

      Try
        Dim pageSize As Integer = _Connection.PageSize
        Dim pageStartIndex As Integer = 0
        Dim pageStopIndex As Integer = pageStartIndex + pageSize - 1
        Dim onLastPage As Boolean = False
        Dim allValuesRetrieved As Boolean = False

        Dim filter As String = String.Format("(distinguishedName={0})", adsPath.DnValue)
        searcher = _Connection.CreateDirectorySearcher(filter, adsPath.Parent.ToString, DirectoryServices.SearchScope.OneLevel)

        Do
          Dim attributeWithRange As String
          If onLastPage Then
            attributeWithRange = String.Format("{0};range={1}-*", attributeName, pageStartIndex)
          Else
            attributeWithRange = String.Format("{0};range={1}-{2}", attributeName, pageStartIndex, pageStopIndex)
          End If

          searcher.PropertiesToLoad.Clear()
          searcher.PropertiesToLoad.Add(attributeWithRange)
          Dim results As DirectoryServices.SearchResult = searcher.FindOne

          If results.Properties.Contains(attributeWithRange) Then
            For Each itemValue As Object In results.Properties(attributeWithRange)
              objList.Add(itemValue)
            Next

            If onLastPage Then
              allValuesRetrieved = True
            Else
              pageStartIndex = pageStopIndex + 1
              pageStopIndex = pageStartIndex + pageSize - 1
            End If
          Else
            onLastPage = True
          End If
        Loop Until allValuesRetrieved
      Catch ex As Exception
        Throw ex
      Finally
        If searcher IsNot Nothing Then _Connection.FreeResource(searcher)
      End Try

      'Copy list into array
      If objList.Count > 0 Then
        Dim arrayValues(objList.Count - 1) As Object
        objList.CopyTo(arrayValues, 0)
        Return arrayValues
      Else
        Return Nothing
      End If
    End Function
#End Region

#Region "Implement AdDataReader"
    Public Overrides Sub Close()
      If Not Me.IsClosed Then
        _IsClosed = True

        If _Searcher IsNot Nothing Then
          _Connection.FreeResource(_Searcher)
          _Searcher = Nothing
        End If

        If _Command IsNot Nothing _
        AndAlso _Connection IsNot Nothing _
        AndAlso IsCommandBehavior(System.Data.CommandBehavior.CloseConnection) Then
          _Connection.Close()
        End If
      End If
    End Sub

    ''' <summary>
    ''' Gets a value indicating the depth of nesting for the current row.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overrides ReadOnly Property Depth As Integer
      Get
        Return 0
      End Get
    End Property

    ''' <summary>
    ''' Gets the number of columns in the current row.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overrides ReadOnly Property FieldCount As Integer
      Get
        Return _RecordSetParser.Parameters.Count
      End Get
    End Property

    ''' <summary>
    ''' Gets the value of the specified column as a Boolean.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetBoolean(ByVal ordinal As Integer) As Boolean
      Return AdConvert.ToBoolean(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a byte.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetByte(ByVal ordinal As Integer) As Byte
      Return AdConvert.ToByte(Item(ordinal))
    End Function

    ''' <summary>
    ''' Reads a stream of bytes from the specified column, starting at location
    ''' indicated by dataOffset, into the buffer, starting at the location
    ''' indicated by bufferOffset.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <param name="dataOffset">
    ''' The index within the row from which to begin the read operation.
    ''' </param>
    ''' <param name="buffer">
    ''' The buffer into which to copy the data.
    ''' </param>
    ''' <param name="bufferOffset">
    ''' The index with the buffer to which the data will be copied.
    ''' </param>
    ''' <param name="length">
    ''' The maximum number of characters to read.
    ''' </param>
    ''' <returns>
    ''' The actual number of bytes read.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function GetBytes(ByVal ordinal As Integer, ByVal dataOffset As Long, ByVal buffer() As Byte, ByVal bufferOffset As Integer, ByVal length As Integer) As Long
      Dim fieldType As Type = Me.GetFieldType(ordinal)
      If fieldType Is GetType(String) Then
        buffer = AdConvert.ToByteArray(Item(ordinal))
      Else
        buffer = AdConvert.HexToByteArray(Item(ordinal).ToString)
      End If

      Return 0
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a single character.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetChar(ByVal ordinal As Integer) As Char
      Return AdConvert.ToChar(Item(ordinal))
    End Function

    ''' <summary>
    ''' Reads a stream of characters from the specified column,
    ''' starting at location indicated by dataIndex, into the buffer,
    ''' starting at the location indicated by bufferIndex.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <param name="dataOffset">
    ''' The index within the row from which to begin the read operation.
    ''' </param>
    ''' <param name="buffer">
    ''' The buffer into which to copy the data.
    ''' </param>
    ''' <param name="bufferOffset">
    ''' The index with the buffer to which the data will be copied.
    ''' </param>
    ''' <param name="length">
    ''' The maximum number of characters to read.
    ''' </param>
    ''' <returns>
    ''' The actual number of characters read.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function GetChars(ByVal ordinal As Integer, ByVal dataOffset As Long, ByVal buffer() As Char, ByVal bufferOffset As Integer, ByVal length As Integer) As Long
      Return AdConvert.ToChars(Item(ordinal), dataOffset, buffer, bufferOffset, length)
    End Function

    ''' <summary>
    ''' Gets the name of the data type of the specified column.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <returns>
    ''' A string representing the name of the data type.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function GetDataTypeName(ByVal ordinal As Integer) As String
      Return GetFieldType(ordinal).Name
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a DateTime object.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetDateTime(ByVal ordinal As Integer) As Date
      Return AdConvert.ToDateTime(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a Decimal object.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetDecimal(ByVal ordinal As Integer) As Decimal
      Return AdConvert.ToDecimal(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a double-precision floating point number.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetDouble(ByVal ordinal As Integer) As Double
      Return AdConvert.ToDouble(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets an IEnumerator that can be used to iterate through the rows in the data reader.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overrides Function GetEnumerator() As System.Collections.IEnumerator
      Return New System.Data.Common.DbEnumerator(Me, IsCommandBehavior(System.Data.CommandBehavior.CloseConnection))
    End Function

    ''' <summary>
    ''' Gets the data type of the specified column.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetFieldType(ByVal ordinal As Integer) As System.Type
      GetSchemaTable()
      Return DirectCast(_Schema.Rows(ordinal).Item("DataType"), System.Type)
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a single-precision floating point number.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetFloat(ByVal ordinal As Integer) As Single
      Return AdConvert.ToFloat(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a globally-unique identifier (GUID).
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetGuid(ByVal ordinal As Integer) As System.Guid
      Return AdConvert.ToGuid(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 16-bit signed integer.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetInt16(ByVal ordinal As Integer) As Short
      Return AdConvert.ToInt16(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 32-bit signed integer.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetInt32(ByVal ordinal As Integer) As Integer
      Return AdConvert.ToInt32(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as a 64-bit signed integer.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetInt64(ByVal ordinal As Integer) As Long
      Return AdConvert.ToInt64(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the column name of the given ordinal position.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks></remarks>
    Public Overrides Function GetName(ByVal ordinal As Integer) As String
      Dim propertyIndex As Integer = 0
      For Each parm As AdParameter In _RecordSetParser.Parameters
        If propertyIndex = ordinal Then
          Return parm.ParameterName
        Else
          propertyIndex += 1
        End If
      Next

      Return String.Empty
    End Function

    ''' <summary>
    ''' Gets the column ordinal given the name of the column.
    ''' </summary>
    ''' <param name="name">
    ''' The name of the column.
    ''' </param>
    ''' <remarks>
    ''' Returns the position of the ordinal if the column name is found, a negative value otherwise.
    ''' </remarks>
    Public Overrides Function GetOrdinal(ByVal name As String) As Integer
      Dim propertyIndex As Integer = 0
      For Each parm As AdParameter In _RecordSetParser.Parameters
        If String.Compare(parm.ParameterName, name, True) = 0 Then
          Return propertyIndex
        Else
          propertyIndex += 1
        End If
      Next

      Return -1
    End Function

    ''' <summary>
    ''' Returns a DataTable that describes the column metadata of the DbDataReader.
    ''' </summary>
    ''' <returns>
    ''' A DataTable that describes the column metadata.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function GetSchemaTable() As System.Data.DataTable
\      If _Schema Is Nothing Then
        'Get Class Name and Property Names
        Dim className As String = _RecordSetParser.ObjectCategory
        Dim propertyNames(_RecordSetParser.Parameters.Count - 1) As String
        For parmIndex As Integer = 0 To _RecordSetParser.Parameters.Count - 1
          propertyNames(parmIndex) = _RecordSetParser.Parameters.Item(parmIndex).ParameterName
        Next

        'Track Schema Information
        Dim schema As New ActiveDirectory.AdSchemaBuilder(_Connection)
        _Schema = schema.Build(className, propertyNames)

        'Reset multi-valued attribute metadata
        _MultiValuedAttributes = Nothing
      End If

      Return _Schema
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as an instance of String.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overrides Function GetString(ByVal ordinal As Integer) As String
      Return AdConvert.ToString(Item(ordinal))
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as an instance of Object
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <returns>
    ''' The value of the specified column.
    ''' </returns>
    ''' <remarks>
    ''' Multi-valued attributes are always returned as an array of objects and
    ''' single-valued attributes as an object.
    ''' </remarks>
    Public Overrides Function GetValue(ByVal ordinal As Integer) As Object
      Return Item(ordinal)
    End Function

    ''' <summary>
    ''' Pouplates an instantiated array of objects with the column values of the current row.
    ''' </summary>
    ''' <param name="values">
    ''' An array of Object into which to copy the attribute columns.
    ''' </param>
    ''' <returns>
    ''' The number of instances of Object in the array.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function GetValues(ByVal values() As Object) As Integer
      ThrowErrorOnInvalidRead()

      Dim propertyIndex As Integer = 0
      Dim maxFieldsToRead As Integer = _CurrentRow.Properties.PropertyNames.Count
      If values.Length < maxFieldsToRead Then maxFieldsToRead = values.Length
      For Each parm As AdParameter In _RecordSetParser.Parameters
        values(propertyIndex) = Item(parm.ParameterName)
        propertyIndex += 1
      Next

      Return maxFieldsToRead
    End Function

    ''' <summary>
    ''' Gets a value that indicates whether this DbDataReader contains one or more rows.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overrides ReadOnly Property HasRows As Boolean
      Get
        Return _HasRows
      End Get
    End Property
    Private _HasRows As Boolean

    ''' <summary>
    ''' Gets a value indicating whether the DbDataReader is closed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overrides ReadOnly Property IsClosed As Boolean
      Get
        If _Connection.State = ConnectionState.Closed Then _IsClosed = True
        Return _IsClosed
      End Get
    End Property

    ''' <summary>
    ''' Gets a value that indicates whether the column contains nonexistent or missing values.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <returns>
    ''' True if the specified column is equivalent to DBNull, false otherwise.
    ''' </returns>
    ''' <remarks></remarks>
    Public Overrides Function IsDBNull(ByVal ordinal As Integer) As Boolean
      Return Item(ordinal) Is DBNull.Value
    End Function

    ''' <summary>
    ''' Gets the value of the specified column as an instance of Object.
    ''' </summary>
    ''' <param name="ordinal">
    ''' The zero-based column ordinal.
    ''' </param>
    ''' <remarks>
    ''' Multi-valued attributes are always returned as an array of objects and
    ''' single-valued attributes as an object.
    ''' </remarks>
    Default Public Overloads Overrides ReadOnly Property Item(ByVal ordinal As Integer) As Object
      Get
        ThrowErrorOnInvalidRead()
        Return Item(GetName(ordinal))
      End Get
    End Property

    ''' <summary>
    ''' Gets the value fo the specified column as an instance of Object.
    ''' </summary>
    ''' <param name="name">
    ''' The name of the column.
    ''' </param>
    ''' <remarks>
    ''' Multi-valued attributes are always returned as an array of objects and
    ''' single-valued attributes as an object.
    ''' </remarks>
    Default Public Overloads Overrides ReadOnly Property Item(ByVal name As String) As Object
      Get
        ThrowErrorOnInvalidRead()

        Dim propertyValues As System.DirectoryServices.ResultPropertyValueCollection
        propertyValues = _CurrentRow.Properties.Item(name)

        If IsFieldMultiValued(name) Then
          If _Connection.PageSize <= 500 _
          OrElse propertyValues.Count <= _Connection.PageSize - 25 Then
            Dim arrayValues(propertyValues.Count - 1) As Object
            propertyValues.CopyTo(arrayValues, 0)
            Return arrayValues
          Else
            'If we are close to the paging limit go ahead and use range retrieval to be safe.
            'Note: Anytime range retrieval must be used performance will be negatively impacted.
            Dim dsPath As New Path(_CurrentRow.Properties.Item("ADsPath").Item(0).ToString)
            Return RangeRetrieval(dsPath, name)
          End If
        ElseIf propertyValues.Count > 0 Then
          Return propertyValues.Item(0)
        Else
          Return Nothing
        End If
      End Get
    End Property

    ''' <summary>
    ''' Advances the reader to the next result when reading the results of a batch statements.
    ''' </summary>
    ''' <returns>
    ''' Returns true if there are more resultsets, false otherwise.
    ''' </returns>
    ''' <remarks>
    ''' This method allows you to process multiple resultsets returned when a batch
    ''' is submitted to the data provider.
    ''' </remarks>
    Public Overrides Function NextResult() As Boolean
      ThrowErrorOnInvalidReadNext()

      'Free Resources
      If _Searcher IsNot Nothing Then
        _Connection.FreeResource(_Searcher)
        _Searcher = Nothing
      End If

      'Initialize RecordSet
      _Schema = Nothing
      _RecordsAffected = 0
      _CurrentSetIndex += 1
      _CurrentRowIndex = -1
      _CurrentSet = Nothing
      _RecordSetParser = Nothing

      Dim setsAvailable As Boolean = _CurrentSetIndex < _Command.InnerCommandSets.Count
      If setsAvailable Then
        DirectCast(_Connection, AdConnection).OnStateChange(ConnectionState.Executing)

        _RecordSetParser = _Command.InnerCommandSets(_CurrentSetIndex)
        If (_RecordSetParser.CommandType Or StatementTypes.Delete) = StatementTypes.Delete _
        OrElse (_RecordSetParser.CommandType Or StatementTypes.Select) = StatementTypes.Select _
        OrElse (_RecordSetParser.CommandType Or StatementTypes.Update) = StatementTypes.Update Then
          _Searcher = _Command.CreateSearcher(_CurrentSetIndex)
          _CurrentSet = _Searcher.FindAll()
          _HasRows = _CurrentSet IsNot Nothing
        Else
          _HasRows = False
        End If

        DirectCast(_Connection, AdConnection).OnStateChange(ConnectionState.Open)
      End If

      Return setsAvailable
    End Function

    ''' <summary>
    ''' Advances the reader to the next record in a result set.
    ''' </summary>
    ''' <returns>
    ''' Returns true if there are more rows, false otherwise.
    ''' </returns>
    ''' <remarks>
    ''' The default position of a data reader is before the first record.
    ''' Therefore, you must call Read to begin accessing data.
    ''' </remarks>
    Public Overrides Function Read() As Boolean
      If _CurrentSet IsNot Nothing Then
        _CurrentRowIndex += 1

        If _CurrentSet.Count > _CurrentRowIndex Then
          DirectCast(_Connection, AdConnection).OnStateChange(ConnectionState.Fetching)
          _CurrentRow = _CurrentSet.Item(_CurrentRowIndex)
          _RecordsAffected += 1
        Else
          _CurrentRow = Nothing
        End If
      End If

      _HasRows = _CurrentSet IsNot Nothing AndAlso _CurrentRow IsNot Nothing
      If Not HasRows AndAlso _CurrentSetIndex + 1 >= _Command.InnerCommandSets.Count Then Close()
      Return _HasRows
    End Function

    ''' <summary>
    ''' Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.
    ''' </summary>
    ''' <remarks>
    ''' The RecordsAffected property is not set until all rows are read and you close the SqlDataReader.
    ''' </remarks>
    Public Overrides ReadOnly Property RecordsAffected As Integer
      Get
        Return _RecordsAffected
      End Get
    End Property
#End Region
  End Class
End Namespace
Advertisement

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: