Blue Theme Orange Theme Green Theme Red Theme
 
Discover the top 5 tips for understanding .NET Interop
Home | Forums | Videos | Advertise | Certifications | Downloads | Blogs | Interviews | Jobs | Beginners | Training
 | Consulting  
Submit an Article Submit a Blog 
 Jump to
Skip Navigation Links
TechnologyExpand Technology
WebsiteExpand Website
Discover the top 5 tips for understanding .NET Interop
Search :       Advanced Search »
Home » Active Directory C# » .NET and Active Directory

.NET and Active Directory

This article outlines connecting to AD from .Net using ADO and System.DirectoryServices and also forming a unique ID from AD, moving entries from AD to a database and forming AD queries (also date formatting for AD).

Page Views : 81506
Downloads : 0
Rating :
 Rate it
Level : Beginner
   Print Read/Post comments Post a comment  Similar Articles  
   Email to a friend  Bookmark  Author's other articles  
 
Discover the top 5 tips for understanding .NET Interop
Become a Sponsor
6 Months Free & No Setup Fees ASP.NET Hosting!
Become a Sponsor
 Tag Cloud
 Latest Jobs
More ... 
 Latest Interview Questions
More ... 

Introduction:

When we had the requirement to read the active directory from .Net, for a project, we were left with many questions and grey areas on mind; sadly there was not much info available on googling. After a thorough study by creating many proof of concepts we got a bit comfortable with the same, I thought why not put it all onto the web for the use of any people who want to access the Active directory through .Net. 

Here I will discuss the multiple options available to connect to active directory, and also the patterns of AD queries, and other small findings that I came across while doing all this.

LDAP and GC:

Now why do we need to know these two jargons? - Simply because these are the two ways you can connect to the active directory. For the purpose of correct documentation the abbreviations for the two are given below.

LDAP - Lightweight directory access protocol.
GC - Global Catalog.

The conventional definition for LDAP is as follows:

  • A set of protocols for accessing information directories

The LDAP is a full replica of a single domain and that "GC:" is a partial replica of all domains in the forest.

The global catalog has a database table like structure which helps in faster searches. The Global Catalog contains directory data for all domains in a forest. The Global Catalog contains a partial replica of every domain directory. It contains an entry for every object in the enterprise forest, but does not contain all the properties of each object. Instead, it contains only the properties specified for inclusion in the Global Catalog. The global catalog is created by replicating from all the domains in a forest of the active directory on a periodical basis.

An LDAP and GC path would look something like these.

  • "LDAP://<host name>/<object name>" 
  • "GC://<host name>/<object name>"

In the examples above, "LDAP:" specifies the LDAP provider. "GC:" uses the LDAP provider to bind to the Global Catalog service to execute fast queries.

If you were well accustomed with how the active directory is set up, the above lines would give you a clear insight on what a GC and LDAP is. For an overview on the Active directory structure (what is a forest? what is a domain? etc) please refer
http://www.windowsitlibrary.com/Content/155/07/toc.html.

Having known what are the two different ways the AD (hereon called AD instead of Active directory), allows itself to be contacted, lets look at how our other party ".NET" can interact with Active Directory and before that how to query AD.

Forming Queries for Active directory:

The LDAP search strings used to query Active directory is a little different from the normal SQL queries we would write on databases.

These queries are based on one or more of the key attributes as follows

ObjectCategory:

This could be 'user' or 'printer' or any defined category in the AD. If you would be searching only users then this value needs to be set to user e.g (objectCategory=user). By specifying this search narrows down, and you can expect to see results sooner.

AD attributes:

There is a big list of fields that can be used in Activedirectory, apart from the extensive set it provides, ad administrators can add their own fields. The query can consist of any of the named fields of AD.e.g. (samAccountname=john.abraham).

To combine the criteria, the normal bitwise operators (&, |, !) can be used,

For example, if I want to query for all the users whose distinguishedName begin with 'john' my query would look like this (&(objectCategory=user)(cn=john*))

If I wanted to find how many users whose names begin either with jack or jill this is how I would frame my query.
(&(objectCategory=user)(|(cn=jack*)(cn=jill*))).

Users whose mailed is empty.
(&(objectCategory=user)(!(mail=*)))

For more such examples you could refer http://www.petri.co.il/ldap_search_samples_for_windows_2003_and_exchange.htm.
http://www.microsoft.com/technet/scriptcenter/guide/sas_ads_emwf.mspx?mfr=true

For specifying this search you need to be aware of the available properties/fields in your active directory.

So much for queries, but then if you want to involve dates in your query, there is bit more of a job to be done, i.e. AD doesn't accept our normal date format for the queries, so the date needs to be converted into a AD readable format. Pls find below a function to do the same. 

/// <summary>

/// Method to convert date to AD format

/// </summary>

/// <param name="date">date to convert</param>

/// <returns>string containing AD formatted date</returns>

private static string ToADDateString(DateTime date)

{

    string year = date.Year.ToString();

    int month = date.Month;

    int day = date.Day;

    int hr = date.Hour;

    string sb = string.Empty;

    sb += year;

    if (month < 10)

    {

        sb += "0";

    }

    sb += month.ToString(); bv                              

    if (day < 10)

    {

        sb += "0";

    }

    sb += day.ToString();

    if (hr < 10)

    {

        sb += "0";

    }

    sb += hr.ToString();

    sb += "0000.0Z";

    return sb.ToString();

} 

Another challenge was to find a unique ID from AD which can be used as a primary key in cases when we need to take a snapshot of the AD to the database. You might wonder that the email id or aliasname of a person should be unique, but in certain organizations when  request for an extra mailbox or any service account is raised, the additional account is created in the same name as the existing one, thus making those attributes unusable as unique Ids.

In AD the unique id used is the objectGUID which cannot be directly inserted into the database as it's a 128 bit octet string. To store this into a Database it needs be converted to a readable format for example a binary string. Find below a code snippet that would do just that.   

byte[] arraybyte = (byte[])de.Properties["objectGUID"].Value;

StringBuilder OctetToHexStr = new StringBuilder();

for (int k = 0; k < arraybyte.Length; k++)

{

    OctetToHexStr.Append(@"\" + Convert.ToString(Convert.ToByte(arraybyte[k]),16));

}

ADO and System.DirectoryServices :

From .NET there are two that you can connect to AD, one is through our good old ADO, which we have used from age-old days, and the other is through the .Net provided namespace System.DirectoryServices.

By using ActiveX data objects (or ADO as its more popularly known) you can connect to AD, as you would with any other database. The connection provider you would use to do so is "ADsDSOObject".

The other basic objects that you would require to establish a connection to AD and query it would be the connection object, recordset object and lastly the command object to maintain active connections, specify query parameters such as page size, search scope and so on.

Since our focus here is using System.DirectoryServices, we will leave the ADO part aside. But just as an intro, find below the code to establish connection through ADO.

objConnection = CreateObject("ADODB.Connection");

objConnection.Provider = "ADsDSOObject";

objConnection.Properties("User ID").Value = "myUser";

objConnection.Properties("Password").Value = "myPassword";

objConnection.Properties("Encrypt Password") = true;

objConnection.Open("Active Directory Provider");


Now that we are done with creating a connection object, we can proceed on to create a command object to query AD. The code snippet to create a command object for the above given connection object would be as follows.

ADODB.Command objCommand = new ADODB.Command();

objCommand.ActiveConnection = objConnection;

strBase = "<LDAP://OU=User Directory,DC=asia,DC=myDomain,DC=com>";


Having set the connection, we are now left with the task of reading the AD. Now we need to set a query to read the AD, just as we would specify a Sql for a database, a query needs to be passed to the active directory for it to pick the objects that match the query.

Suppose I want the list of users created after a certain date say strDate, then my AD query would look something like this.

string strFilter ="(&(objectCategory=user)(objectClass=user)(whenCreated>=" + strFromDate +"))";


The following is sample code to do the same.

ADODB.Recordset rsAD = new ADODB.RecordsetClass();  

try

{

    rsAD.Open(strFilter,adConn,ADODB.CursorTypeEnum.adOpenForwardOnly,ADODB.LockTypeEnum.adLockReadOnly,0);

}

catch (Exception exp)

{

    Response.Write(exp.Message);

    Response.End();

}  

DataTable userDataTable = new DataTable();

userDataTable.Columns.Add ("AccountName");

userDataTable.Columns.Add ("CommonName");

userDataTable.Columns.Add ("CreatedDate"); 

while(!rsAD.EOF)

{  

    DataRow newRow = userDataTable.NewRow(); 

    newRow[0] = rsAD.Fields[0].Value;

    newRow[1] = rsAD.Fields[1].Value;

    newRow[2] = rsAD.Fields[2].Value;

    userDataTable.Rows.Add(newRow);

    rsAD.MoveNext();

}

Having filled up our Datatable with the records from AD, we can choose to display it in any format (DataList, DataReader or Datagrid).

System.DirectoryServices:

In this namespace the most used classes are the DirectoryEntry and DirectorySearcher.

As the name suggests DirectoryEntry would represent each entry of the AD, be it a user or a printer or any such resource.
The DirectorySearcher class helps in querying the AD. Please follow the inline comments for details on code.

using System;

using System.Collections;

using System.DirectoryServices;

using System.Data;

using System.Security.Permissions;

using System.IO;

using System.Text;

[assembly: SecurityPermission(SecurityAction.RequestMinimum, Unrestricted = true)]

namespace Web.Apps.ADInterface

{

    /// <summary>

    /// Class to interface with AD and search for new, modified and deleted users.

    /// </summary>

    public class ADSearch

    {

        #region Private Variables

        private static string _gcPath = "GC://mydomain.com";

        private static string _serviceAccountName = @"Europe\abcsdfs-S";

        private static string _servicePassword = "2$%^&*()";

        private DirectoryEntry entry = new DirectoryEntry();

        #endregion

        #region Constructor

        private ADSearch()

        {

            entry.Path = _gcPath;

            entry.Username = _serviceAccountName;

            entry.Password = _servicePassword;

        }

        #endregion

        #region Methods

        /// <summary>

        /// Method to Search for new,Modified and Deleted users

        /// </summary>

        /// <param name="createdDate"></param>

        public static void SearchADUsers(DateTime createdDate, string path)

        {

            string strFilter = string.Empty;

            string strFromDate = ToADDateString(Convert.ToDateTime(createdDate));

 

            //Search criteria for fetching users whose account name, mail and distinguished name 
            are not empty and whose entries are changed since the specified date (either created
            or modified after the specified date)
 

 

            strFilter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*)
            (|(whenChanged>="
+ strFromDate + ")(whenCreated>=" + strFromDate + ")))";

 

            ADSearchUsers(strFilter, path);

        }

        /// <summary>

        /// Method to Search for new,Modified and Deleted users

        /// </summary>

        public static void TakeADSnapshot()

        {

            string filter = string.Empty;

            filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))";

            ADSnapshot(filter, @"C:\insert.CSV");

        }

 

        /// <summary>

        /// Method to Search for new,Modified and Deleted users

        /// </summary>

        /// <param name="path">CSV file Path</param>

        public static void TakeADSnapshot(string path)

        {

            string filter = string.Empty;

            filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))";

            ADSnapshot(filter, path);

        }

        //The function below takes a snapshot of AD users who satisfy the specified criteria and 
        constructs a  CSV file out of it, This is done  it's the easiest way to  move it into a database.

        /// <summary>

        /// Method to get take an AD snapshot

        /// </summary>

        /// <param name="filterString">AD Search string</param>

        /// <param name="path">Path of CSV file</param>

        private static void ADSearchUsers(string filterString, string path)

        {

 

            DirectoryEntry entry = new DirectoryEntry();

            entry.Path = _gcPath;

            entry.Username = _serviceAccountName;

            entry.Password = _servicePassword;

 

            DirectorySearcher mySearcher = new DirectorySearcher(entry);

            mySearcher.Filter = filterString.ToString();

            TextWriter tw = new StreamWriter(path, true);

            mySearcher.PageSize = 10;

            mySearcher.CacheResults = false;

            StringBuilder sqlinsert = null;

 

            //Add all properties that need to be fetched    

            mySearcher.PropertiesToLoad.Add("displayName");

            mySearcher.PropertiesToLoad.Add("givenname"); ;

            mySearcher.PropertiesToLoad.Add("sn");

            mySearcher.PropertiesToLoad.Add("ou");

            mySearcher.PropertiesToLoad.Add("employeeType");

            mySearcher.PropertiesToLoad.Add("mail");

            mySearcher.PropertiesToLoad.Add("telephoneNumber");

            mySearcher.PropertiesToLoad.Add("samAccountName");

            mySearcher.PropertiesToLoad.Add("whenCreated");

            mySearcher.PropertiesToLoad.Add("whenChanged");

            mySearcher.PropertiesToLoad.Add("objectGUID");

            mySearcher.PropertiesToLoad.Add("c");

 

            //The search scope specifies how deep the search needs to be, it can be either 
            "base"- which means only in the current //level, and "OneLevel" which means the
            base and one level below and then "subtree"-which means the entire tree needs //to be searched.

 

            mySearcher.SearchScope = SearchScope.Subtree;

            SearchResultCollection resultUsers = mySearcher.FindAll();

            int fpos, spos;

            string dn, newdn, newerdn;

            foreach (SearchResult srUser in resultUsers)

            {

                try

                {

                    DirectoryEntry de = srUser.GetDirectoryEntry();

                    byte[] arraybyte = (byte[])de.Properties["objectGUID"].Value;

                    StringBuilder OctetToHexStr = new StringBuilder();

                    for (int k = 0; k < arraybyte.Length; k++)

                    {

                        OctetToHexStr.Append(@"\" + Convert.ToString(Convert.ToByte(arraybyte[k]), 16));

                    }

 

                    dn = de.Properties["distinguishedName"][0].ToString();

                    sqlinsert = new StringBuilder();

                    //To get the domain name from Distinguished name

                    fpos = dn.IndexOf("DC=", 0);

                    newdn = dn.Substring(fpos, dn.Length - fpos);

                    spos = newdn.IndexOf(",DC=", 3);

                    newdn = newdn.Substring(0, spos);

                    newerdn = newdn.Substring("DC=".Length, newdn.Length - 3);

                    sqlinsert.Append(OctetToHexStr.ToString());

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["givenname"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["sn"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["ou"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["employeeType"].Value);

                    sqlinsert.Append(";");

 

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["mail"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["samAccountName"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["c"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["l"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(Convert.ToDateTime(de.Properties["whenChanged"][0].ToString().TrimEnd()).ToString
                    ("dd-MMM-yyyy"));

                    sqlinsert.Append(";");

                    sqlinsert.Append(Convert.ToDateTime(de.Properties["whenCreated"][0].ToString().TrimEnd()).ToString
                    ("dd-MMM-yyyy"));

                    sqlinsert.Append(";");

                    &nbsp; sqlinsert.Append(DateTime.Now.ToString("dd-MMM-yyyy"));

                    sqlinsert.Append(";");

                    sqlinsert.Append(newerdn);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["legacyExchangeDN"].Value);

                    sqlinsert.Append(";");

                    sqlinsert.Append(de.Properties["distinguishedName"].Value);

                    //sqlinsert = OctetToHexStr+ ";" + de.Properties["givenname"].Value + ";" +
                    de.Properties["sn"].Value + ";" + de.Properties["ou"].Value+";" + de.Properties["employeeType"].Value +
                    ";" + +";"+de.Properties["mail"].Value+";"+de.Properties["samAccountName"].Value+";"+ de.Properties
                    ["c"].Value+";"+de.Properties["l"].Value+";"+Convert.ToDateTime(de.Properties["whenChanged"]
                    [0].ToString().TrimEnd()).ToString("dd-MMM-yyyy")+";"+Convert.ToDateTime(de.Properties["whenCreated"]
                    [0].ToString().TrimEnd()).ToString("dd-MMM-yyyy")+";"+DateTime.Now.ToString("dd-MMM-yyyy") +";"
                    +newerdn+";"+de.Properties["legacyExchangeDN"].Value+ ";"+de.Properties["distinguishedName"].Value;

                    de.Close();

                    tw.WriteLine(sqlinsert);

                    sqlinsert.Remove(0, sqlinsert.Length); 

                }

                catch

                {

                    throw;

                }

            }

            tw.Close();

        } 

        #endregion

    }
}


Conclusion

In conclusion I would like to highlight upon some points, it would be useful to keep them in mind while working on Active Directory.

  • Active directory searches would be pretty slow compared to database searches, so it's imperative to narrow down the search criteria as much as possible.
  • To search for deleted users in Active directory could be quite a challenge as the deleted items are physically moved to the obsolete users directory and after a certain "tombstone" period will be permanently deleted. But organisations follow some pattern of identifying deleted user's by certain means like suffixing the samAccountname with-Deleted or prefixing the username with a "_" and so on. Before you do a search on deleted users, it would be worthwhile to consult your AD administrator to know the convention followed.
  • There are much more properties attached to the directorysearcher and directoryentry classes, it would be worthwhile to go through them in msdn.
  • This paper intends to present an insight of connecting to AD using .net, and the code used here are only snippets and not fully working solutions.
  • The class DirectorySearcher would give only a readonly snapshot of AD, to do modifications on the AD you would have to follow different pattern, which is out of scope in this document.
  • Of the methods mentioned using System.DirectoryServices is better than using ADO in .net, nevertheless ADO can be used from VB or ASP.

Happy programming!!!

Comment Request!
Thank you for reading this post. Please post your feedback, question, or comments about this post Here.
Login to add your contents and source code to this article
 [Top] Rate this article
 
 About the author
 
sathya
Looking for C# Consulting?
C# Consulting is founded in 2002 by the founders of C# Corner. Unlike a traditional consulting company, our consultants are well-known experts in .NET and many of them are MVPs, authors, and trainers. We specialize in Microsoft .NET development and utilize Agile Development and Extreme Programming practices to provide fast pace quick turnaround results. Our software development model is a mix of Agile Development, traditional SDLC, and Waterfall models.
Click here to learn more about C# Consulting.
 
Introducing MaxV - one click. infinite control. Hyper-V Hosting from MaximumASP.
Finally – a virtual platform that delivers next-generation Windows Server 2008 Hyper-V virtualization technology from a managed hosting partner you can truly depend on. Visit www.maximumasp.com/max for a FREE 30 day trial. Hurry offer ends soon. Climb aboard the MaxV platform and take advantage of High Availability, Intelligent Monitoring, Recurrent Backups, and Scalability – with no hassle or hidden fees. As a managed hosting partner focused solely on Microsoft technologies since 2000, MaximumASP is uniquely qualified to provide the superior support that our business is built on. Unparalleled expertise with Microsoft technologies lead to working directly with Microsoft as first to offer IIS 7 and SQL 2008 betas in a hosted environment; partnering in the Go Live Program for Hyper-V; and product co-launches built on WS 2008 with Hyper-V technology.
Dynamic PDF
ceTE software specializes in components for dynamic PDF generation and manipulation. The DynamicPDF™ product line allows you to dynamically generate PDF documents, merge PDF documents and new content to existing PDF documents from within your applications.
Discover the Top 5 .NET Memory Management Fundamentals
To write the best .NET code, you need to know exactly how the .NET framework really manages memory. Ricky Leeks presents the Top 5 fundamental facts of .NET memory management. Learn more.
Nevron Chart for .NET 2010.1 Now Available
The leading .NET charting control now features PDF, Flash and Silverlight export, visualization of large datasets and more. Deliver true charting functionality to your BI, Scorecard, Presentation or Scientific apps. Download evaluation now.
ASP.NET 4 Hosting
Get 2 Months Free of ASP.NET Hosting for Only $4.95/month! Receive FREE MS SQL and MySQL Databases Including ASP.NET 4/3.5, MVC 3.0, Silverlight 4, Windows 2008/IIS 7.0 Plus FREE IIS 7 Modules. Host UNLIMITED ASP.NET Web Sites – Click Here!
 
 Post a Feedback, Comment, or Question about this article
Subject:
Comment:
Team Foundation Server Hosting
Become a Sponsor
 Comments
Great Article by Pankaj On April 19, 2007
Are u aware of any way that the ObjectGUID for any AD object can change at any time? If the Active Directory Server changes, etc.? I want to store mappings between the AD user and our system user in a SQL DB. Just wanted to be sure that the mappings stay intact in all cases.
Reply | Email | Modify 
Re: Great Article by sathya On April 20, 2007
Pankaj, objectGUID is the only property that does not change even if an object is moved or renamed, so it can be confidently used as an object identifier.
Reply | Email | Modify 
Great Post! by Ryan On June 3, 2007
Thanks sathya its good to have documentation like this.. i was working recently on a project that requred a lot of ad interactions. i've pondered for a long time and found a good sample code and came across http://www.dotnetactivedirectory.com/ that had a pretty decent wrapper that does many functions for those that are looking. thanks again.
Reply | Email | Modify 
Deleted object by Neeraj On July 2, 2007
Hi Sathya, Do you know how to get deleted object from active directry by C#.Net. Like suppose i deleted the user test1 from active directry & now i want to retrive that test1 user. Thanks Neeraj
Reply | Email | Modify 
Missing code? by Eli On November 8, 2007
Either ADSearchUsers or ADSnapshot seems to be missing. The method there looks like it's writing a snapshot, but if it were renamed then there would be no ADSearchUsers method, and both are required. So what's missing?
Reply | Email | Modify 
Good Article by rohit On December 12, 2011
Thanks
Reply | Email | Modify 
6 Months Free & No Setup Fees ASP.NET Hosting!
 © 2012  contents copyright of their authors. Rest everything copyright Mindcracker. All rights reserved.