I'm using this snippet of code to output a list of all the computers on my network (the language is jscript.net, but it's just a small manipulation of C#).
var parentEntry = new DirectoryEntry();
parentEntry.Path = "WinNT:";
for(var childEntry in parentEntry.Children) {
if(childEntry.SchemaClassName == "Domain") {
var parentDomain = new TreeNode(childEntry.Name);
this.treeView1.Nodes.Add(parentDomain);
var subChildEntry : DirectoryEntry;
var subParentEntry = new DirectoryEntry();
subParentEntry.Path = "WinNT://" + childEntry.Name;
for(subChildEntry in subParentEntry.Children) {
var newNode1 = new TreeNode(subChildEntry.Name);
if(subChildEntry.SchemaClassName == "Computer") {
parentDomain.Nodes.Add(newNode1);
}
}
}
}
I have 2 issues with this:
1) It is extremely slow. There's about 100 computers showing, and it takes about 1 minute to load.
2) I want to get only a list of computers that are currently online.
This can be done because I've seen other programs doing it and they are much faster, also they're able to show only the ones online.
Am I missing something?
I know it's a bit old, but for others...this snippet will return a 760 computer names from a domain using AD within 2-3 seconds....a significant improvement....enjoy!
Friend Shared Function DomainComputers() As Generic.List(Of String)
' Add a Reference to System.DirectoryServices
Dim Result As New Generic.List(Of String)
Dim ComputerName As String = Nothing
Dim SRC As SearchResultCollection = Nothing
Dim Searcher As DirectorySearcher = Nothing
Try
Searcher = New DirectorySearcher("(objectCategory=computer)", {"Name"})
Searcher.Sort = New SortOption("Name", SortDirection.Ascending)
Searcher.Tombstone = False
SRC = Searcher.FindAll
For Each Item As SearchResult In SRC
ComputerName = Item.Properties("Name")(0)
Result.Add(ComputerName)
Next
Catch ex As Exception
End Try
Return Result
End Function
I would look at Linq To Active Directory found on CodePlex
You'd also have to define "my network". Your subnet? Your Organizational Unit? Your domain? Your forest?
Also consider where your LDAP server is that you are querying. Is it close or is it on the other end of a remote link?
Also what do you consider "online"? Do you expect to be able to ping it? Do you expect to be able to connect to it and perform an operation?
There are many things to consider here. Also if you have other infrastructure components such as an SCCM / SMS server they can often be queried much faster since all of the discovery data has flowed up into the data warehouse.
Related
I am trying to scrape all the groups a user is a member of in Active Directory (including groups that they are indirect members of), but I am running into memory exception errors.
Latest error message passed was:
Insufficient memory to continue the execution of the program
The code I run is started in a backgroundworker on a schedule.
I have looked through ALL the SO active directory group scraping posts, and tried as many of the iterations of this process as I could get to work. This one will (usually) work if i don't run it on a schedule. Sometimes it will crash the program though.
Our Active Directory database is quite considerably large.
Any help you could offer with this would be great. I don't mind it taking a bit longer, speed is not an overly great concern, as long as I can get it to perform well, and correctly.
Here's what I have:
DataTable resultsTable = new DataTable();
resultsTable.Columns.Add("EmailID");
resultsTable.Columns.Add("UserID");
resultsTable.Columns.Add("memberOf");
resultsTable.Columns.Add("groupType");
resultsTable.Columns.Add("record_date");
try
{
string RecordDate = DateTime.Now.ToString(format: "dd/MM/yyyy", provider: CultureInfo.CreateSpecificCulture("en-GB"));
string ou = "OU=Groups,OU=nonya,DC=my,DC=domain,DC=net";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, "my.domain.net", ou))
{
using (GroupPrincipal GroupPrin = new GroupPrincipal(context))
{
using (var searcher = new PrincipalSearcher(GroupPrin))
{
using (var results = searcher.FindAll())
{
foreach (var result in results)
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
string GUID = de.Guid.ToString();
string testname1 = de.Name;
string testParentName1 = de.Parent.Name;
string MasterGroupname = testname1.Substring(3, (testname1.Length - 3));
string type = testParentName1.Substring(3, (testParentName1.Length - 3));
using (var group = GroupPrincipal.FindByIdentity(context, IdentityType.Guid, GUID))
{
using (var users = group.GetMembers(true))
{
foreach (Principal user in users)
{
DataRow dr1 = resultsTable.NewRow();
dr1["EmailID"] = user.UserPrincipalName;
dr1["UserID"] = user.SamAccountName;
dr1["memberOf"] = MasterGroupname;
dr1["groupType"] = type;
dr1["record_date"] = RecordDate;
resultsTable.Rows.Add(dr1);
user.Dispose();
}
}
}
}
}
}
}
}
//Table is uploaded to SQL database here, unless it crashes before reaching this point
}
catch (Exception E)
{
logger.Error($"ADGroupScrape error. System says: {E.Message}");
return false;
}
finally
{
resultsTable.Dispose();
}
Disposing each object at the end of the loop will probably solve your problem (result.Dispose()). I've had memory problems with long loops when working with AD. Assuming you're working with thousands of results, and you're allocating memory for each, and there's no break between processing each result, the garbage collector doesn't have an opportunity to clean up for you.
But you're also unnecessarily going back out to AD to search for the group you already found (GroupPrincipal.FindByIdentity). That would also increase memory consumption, but also slow down the whole operation. Instead, just cast result to a GroupPrincipal so you can call .GetMembers() on it.
Unrelated, but helpful: When you have multiple using blocks nested like that, you can combine them all into one block. It saves you indenting so much.
This is what your code would look like with all those suggestions:
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, "my.domain.net", ou))
using (GroupPrincipal GroupPrin = new GroupPrincipal(context))
using (var searcher = new PrincipalSearcher(GroupPrin))
using (var results = searcher.FindAll())
{
foreach (GroupPrincipal result in results)
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
string GUID = de.Guid.ToString();
string testname1 = de.Name;
string testParentName1 = de.Parent.Name;
string MasterGroupname = testname1.Substring(3, (testname1.Length - 3));
string type = testParentName1.Substring(3, (testParentName1.Length - 3));
using (var users = result.GetMembers(true))
{
foreach (Principal user in users)
{
DataRow dr1 = resultsTable.NewRow();
dr1["EmailID"] = user.UserPrincipalName;
dr1["UserID"] = user.SamAccountName;
dr1["memberOf"] = MasterGroupname;
dr1["groupType"] = type;
dr1["record_date"] = RecordDate;
user.Dispose();
}
}
result.Dispose();
}
}
This will probably work, but could probably still be much faster. Using Principal objects (and the whole AccountManagement namespace) is a wrapper around DirectoryEntry/DirectorySearcher which makes things somewhat easier for you, but at the cost of performance. Using DirectoryEntry/DirectorySearcher directly is always faster.
If you want to experiment with that, I wrote a couple articles that will help:
Active Directory: Better performance
Active Directory: Find all the members of a group
So the issue didn't appear to be with my code, but thank you all for the suggestions in cleaning it up.
I changed the Target Platform in Configuration Manager (Visual Studio) to X64, as it was originally set to "Any CPU", and my code now runs successfully every time.
Couple of months down the line and i started getting out of memory exceptions again.
added pagination (which i didnt actually know you could do) and it seems to have resolved the issue. The line i added was ((DirectorySearcher)searcher.GetUnderlyingSearcher()).PageSize = 500; in between the searcher declaration and the searcher.FindAll().
using (var searcher = new PrincipalSearcher(GroupPrin))
{
((DirectorySearcher)searcher.GetUnderlyingSearcher()).PageSize = 500;
using (var results = searcher.FindAll())
{
I am working on a C# code that retrieves all site collection paths from a On-Premise Sharepoint 2013 server. I have the following Site Collections on the server:
/serverurl/
/serverurl/my
/serverurl/my/personal/site1
/serverurl/my/personal/site2
/serverurl/sites/TestSite
/serverurl/custompath/site3
when I run my code , I only get the following site collections:
/serverurl/
/serverurl/my
/serverurl/my/personal/site1
/serverurl/my/personal/site2
I was wondering why my search does not return all the site collections?
here is my code:
ClientContext context = new ClientContext(siteUrl);
var cred = new NetworkCredential(userName, password, domain);
context.Credentials = cred;
KeywordQuery query = new KeywordQuery(context);
query.QueryText = "contentclass:STS_Site";
SearchExecutor executor = new SearchExecutor(context);
query.TrimDuplicates = true;
var resultTable = executor.ExecuteQuery(query);
context.ExecuteQuery();
foreach (var row in resultTable.Value[0].ResultRows)
{
string siteName = row["siteName"] as string;
Console.WriteLine("Site Name: {0}", siteName);
}
Thanks!
I was having the same problem today. I found two solutions.
Regardless if your on-prem or on Office365 we can use Microsoft.Online.SharePoint.Client.Tenant dll. You can use this to get all the Site Collections. You do need your admins to run some power shell if your on-prem. Vesa was nice enough to write a blog about it here
Once you get that done, you can do something like the following (Note:I have not tested this method with a non Admin account) (solution taken from here) Sadly this one will not work for me as I want security trimming and this will code must be ran by a user with tenant read permissions which our users would not normal have.
var tenant = new Tenant(clientContext);
SPOSitePropertiesEnumerable spp = tenant.GetSiteProperties(0, true);
clientContext.Load(spp);
clientContext.ExecuteQuery();
foreach(SiteProperties sp in spp)
{
// you'll get your site collections here
}
I ended up doing this which gets back to using search, I still have a problem, we have well over 500 sites/webs so I'm working with our admins to see if we can increase the max rows search can return. However, the true secret here is TrimDuplicates being set to false, I don't know why SP thinks the results are dups, but it obviously does, so set it to false and you should see all your sits.
KeywordQuery query = new KeywordQuery(ctx);
query.QueryText = "contentclass:\"STS_Site\"";
query.RowLimit = 500;//max row limit is 500 for KeywordQuery
query.EnableStemming = true;
query.TrimDuplicates = false;
SearchExecutor searchExecutor = new SearchExecutor(ctx);
ClientResult<ResultTableCollection> results = searchExecutor.ExecuteQuery(query);
ctx.ExecuteQuery();
var data = results.Value.SelectMany(rs => rs.ResultRows.Select(r => r["Path"])).ToList();
Hope one of the two will work for you.
I'd like to implement a function that searches an ldap server (name, phone number etc.)
Here's what I wrote (the server address is a phony one, but the real one has the same pattern)
DirectoryEntry de = new DirectoryEntry("LDAP://aet7ldap.phony.com")
DirectorySearcher ds = new DirectorySearcher(de);
var test = ds.FindAll();
I know that there are other constructors (string path, string user, string password), but I don't know my username or password and I'm not sure if I need one. So please help me figure out how to do it without these parameters (if that is possible).
I've tried to write a filter as well but that's another thing, because first I need to get my connection right. But can I assume that I have to use these parameters (or column names?) I keep reading everywhere (such as 'gn' for given name and so on)?
sounds like you're trying to connect to Active Directory using the Directory entry method to find a user, you haven't mentioned if you're doing anything to the user you're looking for once they're found, so I'll just give you code that finds the user for you.
using(DirectoryEntry de = new DirectoryEntry("LDAP://servername/DC=phony,DC=com"))
using(DirectorySearcher ds = new DirectorySearcher(de))
{
ds.Filter="(&(objectClass=user)(sAMAccountName="username"))";
//I don't know exactly what criteria you're using to find the user
ds.Filter="(&(objectClass=user)(distinguishedname="")(givenname=""))"
ds.SearchScope = SearchScope.Subtree;
//performing the search and assigning the result to result
SearchResult result = ds.FindOne();
if (result != null)
{
using(DirectoryEntry user = result.GetDirectoryEntry())
{
//put code here to deal with the user as you see fit.
}
lblOutput.Text = "User " + userName + " was found.";
}
}
The Filter is the most important part to find the user you're looking for, the & means and so in the first example I gave in the code above, you're looking for an object with the class user AND the username username. Its easy enough to figure out. Here is a link to a list of all Active Directory attributes, you should be able to find what you're looking for there.
http://www.selfadsi.org/user-attributes.htm
Regards,
Tory Hill
Im wondering how to get a list of all computers / machines / pc from active directory?
(Trying to make this page a search engine bait, will reply myself. If someone has a better reply il accept that )
If you have a very big domain, or your domain has limits configured on how how many items can be returned per search, you might have to use paging.
using System.DirectoryServices; //add to references
public static List<string> GetComputers()
{
List<string> ComputerNames = new List<string>();
DirectoryEntry entry = new DirectoryEntry("LDAP://YourActiveDirectoryDomain.no");
DirectorySearcher mySearcher = new DirectorySearcher(entry);
mySearcher.Filter = ("(objectClass=computer)");
mySearcher.SizeLimit = int.MaxValue;
mySearcher.PageSize = int.MaxValue;
foreach(SearchResult resEnt in mySearcher.FindAll())
{
//"CN=SGSVG007DC"
string ComputerName = resEnt.GetDirectoryEntry().Name;
if (ComputerName.StartsWith("CN="))
ComputerName = ComputerName.Remove(0,"CN=".Length);
ComputerNames.Add(ComputerName);
}
mySearcher.Dispose();
entry.Dispose();
return ComputerNames;
}
What EKS suggested is correct, but is performing a little bit slow.
The reason for that is the call to GetDirectoryEntry() on each result. This creates a DirectoryEntry object, which is only needed if you need to modify the active directory (AD) object. It's OK if your query would return a single object, but when listing all object in AD, this greatly degrades performance.
If you only need to query AD, its better to just use the Properties collection of the result object. This will improve performance of the code several times.
This is explained in documentation for SearchResult class:
Instances of the SearchResult class are very similar to instances of
DirectoryEntry class. The crucial difference is that the
DirectoryEntry class retrieves its information from the Active
Directory Domain Services hierarchy each time a new object is
accessed, whereas the data for SearchResult is already available in
the SearchResultCollection, where it gets returned from a query that
is performed with the DirectorySearcher class.
Here is an example on how to use the Properties collection:
public static List<string> GetComputers()
{
List<string> computerNames = new List<string>();
using (DirectoryEntry entry = new DirectoryEntry("LDAP://YourActiveDirectoryDomain.no")) {
using (DirectorySearcher mySearcher = new DirectorySearcher(entry)) {
mySearcher.Filter = ("(objectClass=computer)");
// No size limit, reads all objects
mySearcher.SizeLimit = 0;
// Read data in pages of 250 objects. Make sure this value is below the limit configured in your AD domain (if there is a limit)
mySearcher.PageSize = 250;
// Let searcher know which properties are going to be used, and only load those
mySearcher.PropertiesToLoad.Add("name");
foreach(SearchResult resEnt in mySearcher.FindAll())
{
// Note: Properties can contain multiple values.
if (resEnt.Properties["name"].Count > 0)
{
string computerName = (string)resEnt.Properties["name"][0];
computerNames.Add(computerName);
}
}
}
}
return computerNames;
}
Documentation for SearchResult.Properties
Note that properties can have multiple values, that is why we use Properties["name"].Count to check the number of values.
To improve things even further, use the PropertiesToLoad collection to let the searcher know what properties you are going to use in advance. This allows the searcher to only read the data that is actually going to be used.
Note that the DirectoryEntry and DirectorySearcher objects should
be properly disposed in order to release all resources used. Its best
done with a using clause.
An LDAP query like: (objectCategory=computer) should do the trick.
if you only want to get the enabled computers:
(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
I have read the following properties from AD,
TerminalServicesProfilePath
TerminalServicesHomeDirectory
TerminalServicesHomeDrive
I've tried DirectoryEntry and DirectorySearcher. But they does not include the properties.
I found some example in vbscript and VC to read them.
However I failed to make it working in C#. Am I missing some tricky thing?
EDIT: Am I have to run it on "Windows Server" to make it works? Can it be read from win XP?
I think you can use the InvokeGet method on your DirectoryEntry, passing in the name of the property you want to read.
2008-12-10 11:50 CET — Edited in response to the comment
If I specify a garbage property name, I get the same COM exception. Are you sure the properties you're trying to retrieve are part of the AD schema?
Just to make sure, the code I'm using is as follows:
using (DirectorySearcher searcher = new DirectorySearcher("(cn=Test)"))
{
SearchResult result = searcher.FindOne();
if (result != null)
{
DirectoryEntry entry = result.GetDirectoryEntry();
string s = entry.InvokeGet("TerminalServicesHomeDrive") as string;
MessageBox.Show(s ?? "null");
}
}
I don't remember exactly, but it's something like this:
//user is a DirectoryEntry
IADsTSUserEx adsiUser = (IADsTSUserEx)user.NativeObject;
then you can get the TerminalServices properties you want via adsiUser.
From my experience you're better off developing on a Windows Server with access to AD due to the libraries you use. Then you'll probably make the above work, too :)
This works for me:
DirectoryEntry user = new DirectoryEntry("LDAP://" + sLDAP_SERVER + "/cn=" + SAMAccount + "," + sLdapFullPath, sUser, sPwd);
//ActiveDs.IADsUser iADsUser = (ActiveDs.IADsUser)user.NativeObject;
ActiveDs.IADsUser cont = null;
cont = user.NativeObject as ActiveDs.IADsUser;
TSUSEREXLib.IADsTSUserEx m_TsUser = (TSUSEREXLib.IADsTSUserEx)cont;
int m_TSLogonDisabled = 0;
m_TsUser.AllowLogon = m_TSLogonDisabled;