On the one hand, to avoid running out of memory requires disposing of your Active Directory objects as soon as you are done with them. On the other hand, if you dispose of all Active Directory objects you will run out of communication ports. In today’s post we will create an Active Directory Connection object aptly named, AdConnection, that will ensure shared connections are used while reducing the risk of running out of memory.
Active Directory Data Access Layer Series
This is the third 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.
- First Post: Active Directory Data Access Layer
- Second Post: Active Directory Connection Strings
- Third Post: AdConnection: Enforcing Active Directory Communication Best Practices
- Fourth Post: AdCommandTextParser: Parsing SQL Statements
- Fifth Post: AdCommand: Running Active Directory Queries
- Sixth Post: AdDataReader: Providing Controlled Access to AD Values
- Seventh Post: AdDataAdapter: Managing Active Directory Data
- Eighth Post: AD Query: Putting it All Together
AdConnection Responsibilities
To reduce the risk of memory leaks and unavailable communication ports it is recommended that a single class be responsible for the creation of DirectoryEntry and DirectorySearcher objects. The AdConnection object will ensure that connections are shared by using the connection string to create a DirectoryEntry object that serves one purpose: establish a connection with the domain. All future actions will use new instances of the DirectoryEntry object that can be safely disposed without affecting the shared connection.
The AdConnection class will track each instance of a DirectoryEntry and DirectorySearcher class. When the connection is closed AdConnection will verify that each object has been properly disposed prior to disposing the shared DirectoryEntry instance.
These Active Directory connection best practice actions all fit nicely within the context of the DbConnection interface. The open method clearly defines when to create the shared DirectoryEntry object. The close method clearly defines when to dispose of all instantiated objects and release memory resources.
The CreateDirectoryEntry and CreateDirectorySearcher objects will be marked Friend to hide their implementation from external libraries. Only the classes within our Active Directory Data Access Layer need use these methods. Of course you can change this to public if you believe it would provide additional benefits outside the system. However, in the spirit of reducing complexity while supporting the Db data model I have opted to hide them.
One last feature of the AdConnection is that it will be used to track connection errors. Since there may be multiple errors per connection we will need a list of error objects.
AdErrorCollection & AdException
These classes are very simple and intuitive to implement. The AdErrorCollection is simply a generic list of AdExceptions. The AdException class inherits from the DbException class. These classes will permit tracking multiple exceptions that may occur per call.
We will begin by creating the AdException class:
Namespace Data.ActiveDirectory Public Class AdException Inherits System.Data.Common.DbException Public Sub New() MyBase.new() End Sub Public Sub New(ByVal message As String) MyBase.New(message) End Sub Public Sub New(ByVal message As String, ByVal errorCode As Integer) MyBase.New(message, errorCode) End Sub Public Sub New(ByVal message As String, ByVal innerException As System.Exception) MyBase.New(message, innerException) Errors.Add(innerException) End Sub Public Sub New(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext) MyBase.New(info, context) End Sub ''' <summary> ''' Set of errors associated with the exception. ''' </summary> ''' <remarks></remarks> Public ReadOnly Property Errors As AdErrorCollection Get If _ErrorCollection Is Nothing Then _ErrorCollection = New AdErrorCollection Return _ErrorCollection End Get End Property Private _ErrorCollection As AdErrorCollection ''' <summary> ''' Get a value that indicates how many records failed. ''' </summary> ''' <remarks></remarks> Public ReadOnly Property RecordsAffected As Integer Get Return _RecordsAffected End Get End Property Private _RecordsAffected As Integer = 0 ''' <summary> ''' Increment the number of records affected by an error. ''' </summary> ''' <remarks></remarks> Friend Sub IncrementRecordsAffected() _RecordsAffected += 1 End Sub End Class End Namespace
Next the very simple AdErrorCollection class:
Namespace Data.ActiveDirectory ''' <summary> ''' Collection of errors associated thrown while executing an active directory command. ''' </summary> ''' <remarks></remarks> Public Class AdErrorCollection Inherits System.Collections.Generic.List(Of System.Exception) End Class End Namespace
AdInfoEventArg
AdInfoEventArg will be used to send messages to the consumer of the connection class. Messages may be informational or may contain exception errors.
Namespace Data.ActiveDirectory Public Class AdInfoEventArg Inherits System.EventArgs Public Sub New() MyBase.New() End Sub Public Sub New(ByVal message As String) _Message = message End Sub Public Sub New(ByVal message As String, ByVal exception As System.Exception) _Message = message _Exception = exception End Sub Public ReadOnly Property Message As String Get Return _Message End Get End Property Private _Message As String Public ReadOnly Property Exception As System.Exception Get Return _Exception End Get End Property Private _Exception As System.Exception End Class End Namespace
AdConnection
The AdConnection object will use the AdConnectionStringBuilder class to parse the connection string and properly connect to the domain. Additionally the Path object will be used to ensure that the path is valid and properly encoded.
Since DbConnection inherits from System.ComponentModel.Component you will need to select the view code option (F7) when opening the file. Simply double clicking on the class will open the designer window which isn’t supported. This is normal behavior.
Another responsibility that will be added to the AdConnection is to ensure that the User ID is valid for the specified connection. This responsibility is in the AdConnection class because the AdConnectionStringBuilder class is responsible for parsing the connection; not verifying that the connection is correct.
When connecting to Active Directory it is easiest to use a fully qualified sAMAccountName or userPrincipalName. Then no matter how the connection is established the account will work. However, if a partial sAMAccountName is used or a distinguished name (yes, a connection can even be established with a distinguished name) then there are specific rules that must be adhered to defining when they can and cannot be used.
Imports System.Configuration Namespace Data.ActiveDirectory ''' <summary> ''' Represents an open connection to an Active Directory Domain Controller. ''' </summary> ''' <remarks></remarks> Public NotInheritable Class AdConnection Inherits System.Data.Common.DbConnection #Region "Definition Section" ''' <summary> ''' The directory entry to retain to ensure the connection will be shared. ''' </summary> ''' <remarks> ''' Connection pooling will occur when: ''' <list> ''' <item>The same server, port and credentials are used.</item> ''' <item>The same authentication flags are used.</item> ''' At least one DirEntry object stays open. ''' </list> ''' </remarks> Private _PoolEntry As System.DirectoryServices.DirectoryEntry ''' <summary> ''' Tracks the connections to the domain ensuring that resources ''' are disposed of correctly when completed. ''' </summary> ''' <remarks></remarks> Private _Connections As System.Collections.ArrayList ''' <summary> ''' Thrown when an information or warning occurs on the connection. ''' </summary> ''' <param name="sender"> ''' The AdConnection object raising the message. ''' </param> ''' <param name="e"> ''' The message to communicate. ''' </param> ''' <remarks></remarks> Event InfoMessage(ByVal sender As Object, ByVal e As AdInfoEventArg) #End Region #Region "Constructor Section" ''' <summary> ''' Instantiates a new instance of AdConnection. ''' </summary> ''' <remarks> ''' Attempts to retrieve connection string information for default domain controller. ''' </remarks> Public Sub New() End Sub ''' <summary> ''' Instantiates a new instance of AdConnection. ''' </summary> ''' <param name="connectionString"> ''' The connection used to open the Active Directory database or the key ''' name referring to the connection string stored in AppSettings or Web.Config. ''' </param> ''' <remarks></remarks> Public Sub New(ByVal connectionString As String) Me.ConnectionString = connectionString End Sub #End Region #Region "Supporting Behaviors Section" ''' <summary> ''' Validates the type of user name supplied with the authentication type. ''' </summary> ''' <param name="userId"> ''' The NT Account, User Principal Name or Distinguished Name to use to login to the domain. ''' </param> ''' <param name="<span class=" />authType"> ''' Defines how to bind to the domain. ''' </param> ''' <returns> ''' True if the user name is valid for the specified authentication type, false otherwise. ''' </returns> ''' <remarks> ''' When connecting to ADAM use the full DN or the UPN. ''' Note the UPN does not require an @ sign in ADAM. ''' </remarks> Private Function IsUserNameValid(ByVal userId As String, ByVal authType As DirectoryServices.AuthenticationTypes) As Boolean If userId Is Nothing Then userId = String.Empty Dim isUPNFormat As Boolean = userId.Contains("@") Dim isQualifiedName As Boolean = userId.Contains("\") Dim isDN As Boolean = userId.ToUpper.Contains(",DC=") Dim isSecure As Boolean = ((authType And DirectoryServices.AuthenticationTypes.Secure) = DirectoryServices.AuthenticationTypes.Secure) If isUPNFormat OrElse isQualifiedName _ OrElse (Not isDN AndAlso isSecure) _ OrElse (isDN AndAlso Not isSecure) Then Return True Else Return False End If End Function ''' <summary> ''' Raises the LarrySteinle.Library.Data.ActiveDirectory.InfoMessage event. ''' </summary> ''' <param name="message"> ''' The message to send to the listener. ''' </param> ''' <remarks></remarks> Friend Sub OnInfoMessage(ByVal message As String) OnInfoMessage(message, Nothing) End Sub ''' <summary> ''' Throw when the connection state changes. ''' </summary> ''' <param name="message"> ''' The message to send to the listener. ''' </param> ''' <param name="exception"> ''' The offending exception. ''' </param> ''' <remarks></remarks> Friend Sub OnInfoMessage(ByVal message As String, ByVal exception As System.Exception) RaiseEvent InfoMessage(Me, New AdInfoEventArg(message, exception)) End Sub #End Region #Region "Resource Management" ''' <summary> ''' Gets or sets the value indicating the number of objects to read at a time. ''' </summary> ''' <remarks> ''' Leave this value at the default of 1000 unless your administrator has ''' changed the default page size on the domain controller. ''' </remarks> Public Property PageSize As Integer = 1000 ''' <summary> ''' Creates a directory entry at the specified distinguished name path. ''' </summary> ''' <param name="path"> ''' A distinguished name to the OU or DC path to connect. ''' </param> ''' <returns> ''' Returns an instantiated directory entry object. ''' </returns> ''' <exception cref="AdException"> ''' Thrown when the system is unable to establish a connection to the domain controller. ''' </exception> ''' <remarks></remarks> Friend Function CreateDirectoryEntry(ByVal path As String) As System.DirectoryServices.DirectoryEntry Return CreateDirectoryEntry(path, True) End Function ''' <summary> ''' Creates a directory entry at the specified distinguished name path. ''' </summary> ''' <param name="path"> ''' The path to the domain and ou to connect. ''' </param> ''' <param name="track"> ''' When true causes the directory entry to be added to the pool ''' ensuring that its resources are released when the connection ''' is closed. When set to false the caller is responsible to ''' free the resource. ''' </param> ''' <returns> ''' Returns an instantiated directory entry object. ''' </returns> ''' <exception cref="AdException"> ''' Thrown when the system is unable to establish a connection to the domain controller. ''' </exception> ''' <remarks> ''' Failure to correctly free resources may result in difficult to detect memory leaks. ''' </remarks> Friend Function CreateDirectoryEntry(ByVal path As String, ByVal track As Boolean) As System.DirectoryServices.DirectoryEntry 'Connect to the directory entry. Dim dirEntry As System.DirectoryServices.DirectoryEntry = Nothing If IsUserNameValid(InnerBuilder.UserId, InnerBuilder.AuthenticationType) Then Dim adPath As New Path(path) 'If a distinguishedName is passed into the routine then convert it to an 'adsiPath supporting SearchRoot distinguishedNames for the Command object. If path.Trim.IndexOf("://") < 0 Then adPath.Provider = InnerBuilder.Provider adPath.HostName = InnerBuilder.DomainName adPath.PortNumber = InnerBuilder.PortNumber.ToString If path.Trim.StartsWith("/") Then adPath.DnValue = path.Trim.Substring(1) Else adPath.DnValue = path.Trim End If End If 'Create the DirectoryEntry Try dirEntry = New System.DirectoryServices.DirectoryEntry(adPath.ToString, InnerBuilder.UserId, InnerBuilder.Password, InnerBuilder.AuthenticationType) If track Then TrackResource(dirEntry) OnStateChange(ConnectionState.Open) Catch ex As Exception If dirEntry IsNot Nothing Then If track Then FreeResource(dirEntry) Else dirEntry.Dispose() dirEntry = Nothing End If End If OnStateChange(ConnectionState.Broken) Throw New AdException("An unexpected error occurred while opening a connection to the domain controller.", ex) End Try Else OnStateChange(ConnectionState.Broken) Throw New AdException("Invalid user id provided for selected authentication type.") End If Return dirEntry End Function ''' <summary> ''' Get an instance of the DirectorySearcher object. ''' </summary> ''' <param name="<span class=" />searchFilter"> ''' The adsi filter to use to search for the objects. ''' </param> ''' <returns> ''' Returns an instance of the DirectorySearcher object. ''' </returns> ''' <remarks></remarks> Friend Function CreateDirectorySearcher(ByVal searchFilter As String) As System.DirectoryServices.DirectorySearcher Dim connectionBuilder As New AdConnectionStringBuilder(ConnectionString) Return CreateDirectorySearcher(searchFilter, connectionBuilder.RootPath, System.DirectoryServices.SearchScope.Subtree) End Function ''' <summary> ''' Get an instance of the DirectorySearcher object. ''' </summary> ''' <param name="<span class=" />searchFilter"> ''' The adsi filter to use to search for the objects. ''' </param> ''' <param name="<span class=" />searchRoot"> ''' Specifies the OU to contain the search scope. ''' </param> ''' <returns> ''' Returns an instance of the DirectorySearcher object. ''' </returns> ''' <remarks></remarks> Friend Function CreateDirectorySearcher(ByVal searchFilter As String, ByVal searchRoot As String) As System.DirectoryServices.DirectorySearcher Return CreateDirectorySearcher(searchFilter, searchRoot, System.DirectoryServices.SearchScope.Subtree) End Function ''' <summary> ''' Get an instance of the DirectorySearcher object. ''' </summary> ''' <param name="searchFilter"> ''' The adsi filter to use to search for the objects. ''' </param> ''' <param name="searchRoot"> ''' Specifies the OU to contain the search scope. ''' </param> ''' <param name="scope"> ''' Specifies the scope of the search. ''' </param> ''' <returns> ''' Returns an instance of the DirectorySearcher object. ''' </returns> ''' <remarks></remarks> Friend Function CreateDirectorySearcher(ByVal searchFilter As String, ByVal searchRoot As String, ByVal scope As System.DirectoryServices.SearchScope) As System.DirectoryServices.DirectorySearcher Dim workEntry As System.DirectoryServices.DirectoryEntry = CreateDirectoryEntry(searchRoot) Dim dirSearcher As System.DirectoryServices.DirectorySearcher = New System.DirectoryServices.DirectorySearcher(workEntry, searchFilter) dirSearcher.ClientTimeout = TimeSpan.FromSeconds(ConnectionTimeout) dirSearcher.PageSize = PageSize dirSearcher.ReferralChasing = DirectoryServices.ReferralChasingOption.All dirSearcher.SearchScope = scope TrackResource(dirSearcher) Return dirSearcher End Function ''' <summary> ''' Adds a resource to the pool. ''' </summary> ''' <param name="resource"> ''' The resource to add to the pool. ''' </param> ''' <remarks></remarks> Private Sub TrackResource(ByVal resource As Object) If TypeOf resource Is System.DirectoryServices.DirectoryEntry _ OrElse TypeOf resource Is System.DirectoryServices.DirectorySearcher Then If _Connections Is Nothing Then _Connections = New System.Collections.ArrayList _Connections.Add(resource) Else Throw New ArgumentException("Invalid resource type provided. Resource must be of type DirectoryEntry or DirectorySearcher.") End If End Sub ''' <summary> ''' Releases the resource removing it from the internal tracking system. ''' </summary> ''' <param name="resource"> ''' The Active Directory resource to release. ''' </param> ''' <remarks></remarks> Friend Sub FreeResource(ByVal resource As System.DirectoryServices.DirectoryEntry) resource.Dispose() _Connections.Remove(resource) resource = Nothing End Sub ''' <summary> ''' Releases the resource removing it from the internal tracking system. ''' </summary> ''' <param name="resource"> ''' The Active Directory resource to release. ''' </param> ''' <remarks></remarks> Friend Sub FreeResource(ByVal resource As System.DirectoryServices.DirectorySearcher) If resource.SearchRoot IsNot Nothing Then FreeResource(resource.SearchRoot) resource.Dispose() _Connections.Remove(resource) resource = Nothing End Sub ''' <summary> ''' Releases com resources to all active connections. ''' </summary> ''' <remarks></remarks> Friend Sub Flush() If _Connections IsNot Nothing Then While _Connections.Count > 0 Dim resource As Object = _Connections(_Connections.Count - 1) If resource Is Nothing Then 'Do Nothing ElseIf TypeOf resource Is System.DirectoryServices.DirectoryEntry Then FreeResource(DirectCast(resource, System.DirectoryServices.DirectoryEntry)) ElseIf TypeOf resource Is System.DirectoryServices.DirectorySearcher Then FreeResource(DirectCast(resource, System.DirectoryServices.DirectorySearcher)) Else If TypeOf resource Is IDisposable Then DirectCast(resource, IDisposable).Dispose() End If _Connections.Remove(resource) End If End While _Connections = Nothing End If End Sub #End Region #Region "Implements IDbConnection Interface" ''' <summary> ''' Raises the System.Data.Common.DbConnection.StateChanged event. ''' </summary> ''' <param name="state"> ''' The current state of the system. ''' </param> ''' <remarks></remarks> Friend Shadows Sub OnStateChange(ByVal state As ConnectionState) If _State <> state Then MyBase.OnStateChange(New StateChangeEventArgs(_State, state)) _State = state End If End Sub ''' <summary> ''' Starts a database transaction. ''' </summary> ''' <param name="isolationLevel"> ''' Specifies the isolation level for the transaction. ''' </param> ''' <returns> ''' An object representing the new transaction. ''' </returns> ''' <remarks></remarks> Protected Overrides Function BeginDbTransaction(ByVal isolationLevel As System.Data.IsolationLevel) As System.Data.Common.DbTransaction Return Nothing End Function ''' <summary> ''' Changes the current database for an open connection. ''' </summary> ''' <param name="databaseName"> ''' Specifies the name of the database for the connection to use. ''' </param> ''' <remarks></remarks> Public Overrides Sub ChangeDatabase(ByVal databaseName As String) Dim isOpen As Boolean = (State = ConnectionState.Open) If isOpen Then Close() Dim dbcon As New System.Data.SqlClient.SqlConnection Dim connectionBuilder As New AdConnectionStringBuilder(ConnectionString) connectionBuilder.DomainController = databaseName ConnectionString = connectionBuilder.ConnectionString If isOpen Then Open() End Sub ''' <summary> ''' Closes the connection to the database. This is the preferred method of closing any open connection. ''' </summary> ''' <remarks></remarks> Public Overrides Sub Close() If _PoolEntry IsNot Nothing Then Flush() _PoolEntry.Dispose() _PoolEntry = Nothing OnStateChange(ConnectionState.Closed) End If End Sub Friend ReadOnly Property InnerBuilder As AdConnectionStringBuilder Get If _InnerBuilder Is Nothing Then _InnerBuilder = New AdConnectionStringBuilder End If Return _InnerBuilder End Get End Property Private _InnerBuilder As AdConnectionStringBuilder ''' <summary> ''' Gets or sets the string used to open the connection. ''' </summary> ''' <remarks></remarks> Public Overrides Property ConnectionString As String Get Return InnerBuilder.ConnectionString End Get Set(ByVal value As String) Dim conString As String Dim conSettings As ConnectionStringSettings = ConfigurationManager.ConnectionStrings.Item(value) Dim appSetting As String = ConfigurationManager.AppSettings.Item(value) If conSettings IsNot Nothing _ AndAlso Not String.IsNullOrWhiteSpace(conSettings.ConnectionString) Then conString = conSettings.ConnectionString ElseIf Not String.IsNullOrWhiteSpace(appSetting) Then conString = appSetting Else conString = value End If _InnerBuilder = New AdConnectionStringBuilder(conString) End Set End Property ''' <summary> ''' Creates and returns a DbCommand object associated with the current connection. ''' </summary> ''' <returns></returns> ''' <remarks></remarks> Protected Overrides Function CreateDbCommand() As System.Data.Common.DbCommand Return DirectCast(New AdCommand(String.Empty, Me), System.Data.Common.DbCommand) End Function ''' <summary> ''' Creates and returns an AdCommand object associated with the current connection. ''' </summary> ''' <returns> ''' An instance of an AdCommand object. ''' </returns> ''' <remarks></remarks> Public Overloads Function CreateCommand() As AdCommand Return New AdCommand(String.Empty, Me) End Function ''' <summary> ''' Gets the name of the current database after a connection is opened, ''' or the database name specified in the connection string before the ''' connection is opened. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public Overrides ReadOnly Property Database As String Get Return InnerBuilder.DomainController End Get End Property ''' <summary> ''' Gets the name of the database server to which to connect. ''' </summary> ''' <remarks></remarks> Public Overrides ReadOnly Property DataSource As String Get Return InnerBuilder.DomainName End Get End Property ''' <summary> ''' Opens a database connection with the settings specified by the ConnectionString. ''' </summary> ''' <remarks></remarks> Public Overrides Sub Open() If State = ConnectionState.Closed Then Try OnStateChange(ConnectionState.Connecting) _PoolEntry = CreateDirectoryEntry(InnerBuilder.RootPath, False) _PoolEntry.RefreshCache() Catch ex As Exception If _PoolEntry IsNot Nothing Then Close() OnStateChange(ConnectionState.Broken) Throw ex End Try End If End Sub ''' <summary> ''' Gets a string that represents the version of the server to which the object is connected. ''' </summary> ''' <remarks>Not supported in our implementation.</remarks> Public Overrides ReadOnly Property ServerVersion As String Get Return String.Empty End Get End Property ''' <summary> ''' Gets a string that describes the state of the connection. ''' </summary> ''' <remarks></remarks> Public Overrides ReadOnly Property State As System.Data.ConnectionState Get If _State <> ConnectionState.Closed AndAlso _PoolEntry Is Nothing Then Return ConnectionState.Broken End If Return _State End Get End Property Private _State As System.Data.ConnectionState = ConnectionState.Closed ''' <summary> ''' Releases all resources used by the Component and optionally releases the managed resources. ''' </summary> ''' <param name="disposing"> ''' True to release both managed and unmanaged resources; false to release only unmanaged resources. ''' </param> ''' <remarks></remarks> Protected Overrides Sub Dispose(ByVal disposing As Boolean) Close() MyBase.Dispose(disposing) End Sub #End Region End Class End Namespace
Summary
Today we learned how to manage active directory objects and connections from a single class to ensure that our connections are shared and resources are properly disposed. This post documented the AdConnection object as part of the Active Directory Data Access Layer series.
Leave a Reply