Introduction
In enterprise organizations, employees/users are maintained by the Active Directory. Sometime, they may need to sync all employees/users to a SharePoint list. Let's say, you wish to develop a Web Part like Know Your Colleague. In this Web Part, Employees/users will be shown as department (IT, Marketing and so on) wise. May be your Active Directory Admin is maintaining it (department wise) already. Now your management has asked you to show these employess/users in SharePoint. The easiest solution can be "Exporting all users in csv file and then import them in a list". So, you have to perform this job every day to keep everything synchronized (User may be deleted or updated).
We need an automated solution which will do all these tasks for us. Currently, SharePoint does not offer any built in feature for it. So this article aims to show you floks how we can achieve it.
Solution
To overcome this, I am going to develop a timer job which will perform following tasks for us periodically.
- Get your desired users from Active Directory
- Save them if does not exist or Update them if it is need
- Delete them if does not exist in Active Directory
I hope you all know how to develop SharePoint timer job. I do not want to show the steps to creating timer job over here because it will elaborate my article unnecessarily. You may check this article for creating timer job.
Let's say, we have a list named Active Directory Users and it has following columns.
Column Name | Mapped AD property |
Full Name | displayName |
Position | title |
Department | department |
UserName | sAMAccountName |
Email | userPrincipalName |
ManagerEmail | will be extract from manager |
WhenModified | whenChanged |
Now our goal is to read users from Active Directory and save them in our list. We will also update/delete items when they will be changed in Active Directory. Let's see how we do this.
Getting AD users using C#
We can get AD users very easily. In that case we need to add System.DirectoryServices
assembly reference in our project. You can perform any operation like create, update, read and delete by this assembly reference. Check for sample code in msdn. I am assuming that you are familiar with AD keywords. For developing this solution, you must be familiar with following keywords. In AD users are differentiated by their distinguished names. Generally, distinguished names look like following. It may vary based container but components are always same.
CN=Atish Dipongkor,OU=IT,DC=atishlab,DC=com
CN: commonName
OU: organizationalUnitName
DC: domainComponent. You have to specify each component separately like Top-Level domain name and Secondly-Level domain name. My domain name is atishlab.com
. So in distinguished name, it is showing something like DC=atishlab
(Second-Level) and DC=com
(Top-Level).
To sync users from AD, at first we have to choose our OU (organizational units). Let's say, we wish to sync users from Junior
OU. The location of Junior
OU looks like following.
atishlab.com
--Department
--IT
--Engineer
--Junior
Basically, we wish to sync our junior engineers from IT department. For getting users, we have to define a path. I mean from where we wish to search for users. If we consider Junior
OU, our path should look like following.
OU=Junior,OU=Engineer,OU=IT,OU=Department,DC=atishlab,DC=com
You may think that path constructing is very complex. Actually, it is not at all. It's so simple. Just think path always will be Bottom to Top. Now your get user method should look like following
using (var directoryInfo = new DirectoryEntry(SyncPath, UserName, Password))
{
var userFindingfilter = "(&(objectClass=user)(objectCategory=person))";
var userProperties = new string[] { "title", "whenChanged", "displayName", "department", "sAMAccountName", "userPrincipalName", "manager" };
using (var directoryInfoSearch = new DirectorySearcher(directoryInfo, userFindingfilter, userProperties, SearchScope.Subtree))
{
var directoryEntryUserSearchResults = directoryInfoSearch.FindAll();
foreach (SearchResult searchResult in directoryEntryUserSearchResults)
{
var searchResultDirectoryEntry = searchResult.GetDirectoryEntry();
if (searchResultDirectoryEntry.Properties["manager"].Value == null)
continue;
var managerDnName = searchResultDirectoryEntry.Properties["manager"].Value.ToString();
var manager = new DirectoryEntry("LDAP://" + managerDnName);
SaveItemIfNotExists(searchResultDirectoryEntry, manager);
}
}
}
Look in my code for few minutes. At first, I have created directoryInfo
object that takes SyncPath
, UserName
and Password
as parameters. In the the SyncPath, you have to append your server url along with OU path.
LDAP:
In my lab environment, the path becomes as following as my server name is WIN-AD
and domain name is atishlab.com
LDAP:
Our next job is to create a DirectorySearcher
and
that has several overloads but I have used following one.
It takes following parameters. Let's discuss one by one.
searchRoot: We have created it already. It should directoryInfo
.
filter: You have to pass a string. It means what you actually want to search. It may Computer, Organizational Unite or User. In our case, we wish to search users. So the filter
should look like following. For other types of filters, please see the documentation.
var userFindingfilter = "(&(objectClass=user)(objectCategory=person))";
propertiesToLoad: AD users have many properties, and you can add your custom properties also. In this paramater, you have to specify what properties you want to load in this search. According to our Active Directory Users list, we need following properties.
var userProperties = new string[] { "title", "whenChanged", "displayName", "department", "sAMAccountName", "userPrincipalName", "manager" };
scope: In which scope, you wish to search. It may be Base
or LevelOne
or Subtree.
As we wish to get all user from Junior
OU (Including its sub OUs), so we have chosen SearchScope.Subtree
.
Now directoryInfoSearch.FindAll()
will return all matched users for us so that we can save them into Active Directory Users list.
Save AD users to SharePoint list by timer job
Now we have to create a timer job so that we can sync our users after a certain interval. I am not going to show the steps of creating timer job here. See the link provided above for creating time job. Also you can see my demo solution. I am starting from Execute
method of our timer job. It looks like following
public override void Execute(Guid targetInstanceId)
{
try
{
SPWebApplication webApplication = this.Parent as SPWebApplication;
var config = WebConfigurationManager.OpenWebConfiguration("/", webApplication.Name);
var syncPath = config.AppSettings.Settings["syncPath"].Value;
var syncUserName = config.AppSettings.Settings["syncUserName"].Value;
var syncPassword = config.AppSettings.Settings["syncPassword"].Value;
var syncSiteUrl = config.AppSettings.Settings["syncSiteUrl"].Value;
var adUserSyncHelper = new AdUserSyncHelper(syncPath, syncUserName, syncPassword, syncSiteUrl);
adUserSyncHelper.Sync();
adUserSyncHelper.RemoveItemsIfNotExistInAd();
}
catch (Exception ex)
{
}
base.Execute(targetInstanceId);
}
I kept my syncPath
, syncUserName
, syncPassword
, syncSiteUrl
in my web application's web.config
file. So open your web.config
and place following things under appSettings
.
<appSettings>
<add key="syncUserName" value="UserName" />
<add key="syncPassword" value="Password" />
<add key="syncPath" value="LDAP://WIN-AD.atishlab.com/OU=Junior,OU=Engineer,OU=IT,OU=Department,DC=atishlab,DC=com" />
<add key="syncSiteUrl" value="http://win-spe:5001/sites/dev" />
</appSettings>
Actually, I have written AdUserSyncHelper
class to sync users. In adUserSyncHelper.Sync()
method, I have iterated over all AD users (desired OU) and save them if user does not exist or update them based on AD whenChanged
property and WhenModified
column of Active Directory Users list. If I found user already exists, then I compaired whenChanged
and WhenModified
to check for modification. If I see user exists in Active Directory Users list but not in AD, then I have deleted it. Actually, I have developed my own workflow to sync users. I hope you will find a better workflow than mine, and you will share with us. So I have done in my AdUserSyncHelper
class is given bellow.
using Microsoft.SharePoint;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SyncAdUserToList
{
class AdUserSyncHelper
{
private string SyncPath { get; set; }
private string UserName { get; set; }
private string Password { get; set; }
private string SiteUrl { get; set; }
public AdUserSyncHelper(string syncPath, string userName, string password, string siteUrl)
{
SyncPath = syncPath;
UserName = userName;
Password = password;
SiteUrl = siteUrl;
}
public void Sync()
{
using (var directoryInfo = new DirectoryEntry(SyncPath, UserName, Password))
{
var userFindingfilter = "(&(objectClass=user)(objectCategory=person))";
var userProperties = new string[] { "title", "whenChanged", "displayName", "department", "sAMAccountName", "userPrincipalName", "manager" };
using (var directoryInfoSearch = new DirectorySearcher(directoryInfo, userFindingfilter, userProperties, SearchScope.Subtree))
{
var directoryEntryUserSearchResults = directoryInfoSearch.FindAll();
foreach (SearchResult searchResult in directoryEntryUserSearchResults)
{
var searchResultDirectoryEntry = searchResult.GetDirectoryEntry();
if (searchResultDirectoryEntry.Properties["manager"].Value == null)
continue;
var managerDnName = searchResultDirectoryEntry.Properties["manager"].Value.ToString();
var manager = new DirectoryEntry("LDAP://" + managerDnName);
SaveItemIfNotExists(searchResultDirectoryEntry, manager);
}
}
}
}
private void SaveItemIfNotExists(DirectoryEntry user, DirectoryEntry manager)
{
using (var spSite = new SPSite(SiteUrl))
{
using (var spWeb = spSite.OpenWeb())
{
spWeb.AllowUnsafeUpdates = true;
var spList = spWeb.Lists["Active Directory Users"];
var spQuery = new SPQuery();
spQuery.Query = @"<Where><Eq><FieldRef Name='UserName' /><Value Type='Text'>" + user.Properties["sAMAccountName"].Value.ToString() + "</Value></Eq></Where>";
var spItems = spList.GetItems(spQuery);
if (spItems.Count == 0)
{
var newItem = spList.AddItem();
newItem["Full Name"] = user.Properties["displayName"].Value == null ? "Not Set" : user.Properties["displayName"].Value.ToString();
newItem["UserName"] = user.Properties["sAMAccountName"].Value.ToString();
newItem["Position"] = user.Properties["title"].Value == null ? "Not Set" : user.Properties["title"].Value.ToString();
newItem["Department"] = user.Properties["department"].Value == null ? "Not Set" : user.Properties["department"].Value.ToString();
newItem["Email"] = user.Properties["userPrincipalName"].Value.ToString();
newItem["ManagerEmail"] = manager.Properties["userPrincipalName"].Value.ToString();
newItem["WhenModified"] = (DateTime) user.Properties["whenChanged"].Value; newItem.Update();
}
else
{
var itemModified = (DateTime) spItems[0]["WhenModified"];
var directoryEntryModified = (DateTime) user.Properties["whenChanged"].Value;
if (directoryEntryModified > itemModified)
{
var existingItem = spItems[0];
existingItem["Full Name"] = user.Properties["displayName"].Value == null ? "Not Set" : user.Properties["displayName"].Value.ToString();
existingItem["UserName"] = user.Properties["sAMAccountName"].Value.ToString();
existingItem["Position"] = user.Properties["title"].Value == null ? "Not Set" : user.Properties["title"].Value.ToString();
existingItem["Department"] = user.Properties["department"].Value == null ? "Not Set" : user.Properties["department"].Value.ToString();
existingItem["Email"] = user.Properties["userPrincipalName"].Value.ToString();
existingItem["ManagerEmail"] = manager.Properties["userPrincipalName"].Value.ToString();
existingItem["WhenModified"] = (DateTime) user.Properties["whenChanged"].Value;
existingItem.Update();
}
}
spWeb.AllowUnsafeUpdates = false;
}
}
}
public void RemoveItemsIfNotExistInAd()
{
using (var spSite = new SPSite(SiteUrl))
{
using (var spWeb = spSite.OpenWeb())
{
var spListItemCollection = spWeb.Lists["Active Directory Users"].Items;
foreach (SPListItem spItem in spListItemCollection)
{
if (!IsItemExistInAd(spItem["UserName"].ToString()))
{
DeleteItem(spItem);
}
}
}
}
}
private void DeleteItem(SPListItem spItem)
{
using (var spSite = new SPSite(SiteUrl))
{
using (var spWeb = spSite.OpenWeb())
{
spWeb.AllowUnsafeUpdates = true;
var spList = spWeb.Lists["Active Directory Users"];
var item = spList.GetItemById(spItem.ID);
item.Delete();
spWeb.AllowUnsafeUpdates = false;
}
}
}
private bool IsItemExistInAd(string sAMAccountName)
{
using (var directoryInfo = new DirectoryEntry(SyncPath, UserName, Password))
{
using (var directoryInfoSearch = new DirectorySearcher(directoryInfo, String.Format("(sAMAccountName={0})", sAMAccountName)))
{
var directoryInfoSearchResult = directoryInfoSearch.FindOne();
if (directoryInfoSearchResult == null) return false;
}
}
return true;
}
}
}
Update 1:
Credit of this update goes to @PIEBALDconsult. He has shared his valuable knowledge for dealing large number of users and date time casting issue. I have updated my code snippet for DateTime issue. Please update my demo source code when you will download it. For dealing large number of users, please follow below links.
- http://geekswithblogs.net/mnf/archive/2005/12/20/63581.aspx
- http://stackoverflow.com/questions/3488394/c-sharp-active-directory-services-findall-returns-only-1000-entries
Points of Interest
I hope I have made myself clear how I did this. So now it is your job to dig into it and discover more and more. Please do not forget to share if you find something new and also let me know where you get stuck.