Hi all I'm porting over my VBScript over to C#. And I ran into a problem that Active Directory properties retrieval is much slower in C#.
This is my incomplete C# code
foreach(string s in dictLast.Keys)
{
if(s.Contains("/"))
str = s.Insert(s.IndexOf('/'), "\\");
else
str = s;
dEntry = new DirectoryEntry("LDAP://" + str);
strUAC = dEntry.Properties["userAccountControl"].Value.ToString();
cmd.CommandText = "INSERT INTO [NOW](readTime) VALUES(\"" + test.Elapsed.Milliseconds.ToString() + "\")";
cmd.ExecuteNonQuery();
test.Reset();
test.Start();
}
If I comment out this line.
strUAC = dEntry.Properties["userAccountControl"].Value.ToString();
It runs at 11 secs. But if I don't, it runs at 2mins 35 secs. The number of records are 3700. On average each record runs at 50 secs. I'm using the Stopwatch Class.
My VBscript runs at only 39 secs (Using difference of Time). With each record either a 0 or 15 milliseconds. I'm using the difference of Timer().
Here's my VBscript
strAttributes = "displayName, pwdLastSet, whenCreated, whenChanged, userAccountControl"
For Each strUser In objList.Keys
prevTime = Timer()
strFilter = "(sAMAccountName=" & strUser & ")"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
Set adoRecordset = adoCommand.Execute
On Error Resume Next
If (adoRecordset.Fields("displayName") = null) Then
strCN = "-"
Else
strCN = adoRecordset.Fields("displayName")
End If
If (Err.Number <> 0) Then
MsgBox(strUser)
End If
strCr8 = DateAdd("h", 8, adoRecordset.Fields("whenCreated"))
strUAC = adoRecordset.Fields("userAccountControl")
If (strUAC AND ADS_UF_DONT_EXPIRE_PASSWD) Then
strPW = "Never expires"
Else
If (TypeName(adoRecordset.Fields("pwdLastSet").Value) = "Object") Then
Set objDate = adoRecordset.Fields("pwdLastSet").Value
dtmPwdLastSet = Integer8Date(objDate, lngBias)
Else
dtmPwdLastSet = #1/1/1601#
End If
If (dtmPwdLastSet = #1/1/1601#) Then
strPW = "Must Change at Next Logon"
Else
strPW = DateAdd("d", sngMaxPwdAge, dtmPwdLastSet)
End If
End If
retTime = Timer() - prevTime
If (objList.Item(strUser) = #1/1/1601#) Then
Wscript.Echo strCN & ";" & strUser & ";" & strPW & ";" & strCr8 & ";" & ObjChange.Item(strUser) & ";0;" & strUAC & ";" & retTime
Else
Wscript.Echo strCN & ";" & strUser & ";" & strPW & ";" & strCr8 & ";" & ObjChange.Item(strUser) & ";" & objList.Item(strUser) & ";" & strUAC & ";" & retTime
End If
Next
Any ideas what's the problem?
Please tell me if I'm not giving enough information. Thank you.
DirectorySearcher way. 1 min 8 secs.
dEntry = new DirectoryEntry("LDAP://" + strDNSDomain);
string[] strAttr = {"userAccountControl"};
foreach(string s in dictLast.Keys)
{
if(s.Contains("/"))
str = s.Insert(s.IndexOf('/'), "\\");
else
str = s;
ds = new DirectorySearcher(de, "(sAMAccountName=" + s + ")", strAttr, SearchScope.Subtree);
ds.PropertiesToLoad.Add("userAccountControl");
SearchResult rs = ds.FindOne();
strUAC = rs.Properties["userAccountControl"][0].ToString();
cmd.CommandText = "INSERT INTO [NOW](readTime) VALUES(\"" + test.Elapsed.Milliseconds.ToString() + "\")";
cmd.ExecuteNonQuery();
test.Reset();
test.Start();
}
where strDNSDomain is the defaultNamingContext. I've tried with domain name but it runs worse, at 3 mins 30 secs.
Looking at someone else's code. Would there be a difference if we omit the domain part?
using (var LDAPConnection = new DirectoryEntry("LDAP://domain/dc=domain,dc=com", "username", "password"))
And just use "LDAP://dc=domain,dc=com" instead.
Work around. Instead of binding each user and getting the properties. I stored all the properties in the first search for lastLogon instead. And output using StreamWriter.
Both DirectoryEntry and ADOConnection use ADSI underlying. There shouldn't be any performance difference.
The only reason for the performance difference is that you are trying to retrieve two different sets of data.
In your VBScript, you are setting ""displayName, pwdLastSet, whenCreated, whenChanged, userAccountControl" to strAttributes. ADSI is going to load these five attributes back from AD only.
In your C# code, you didn't call the RefreshCache method to specify what attributes that you like to load. So, when you access DirectoryEntry.Properties, it automatically calls a RefreshCache() for you without passing in any attributes for you. By default, ADSI will return all non-constructed attributes to you (pretty much all attributes) if you don't specify what attributes to load.
Another problem is that in your VBscript, you run only one LDAP query while in your C# code, you are running many LDAP queries. Each of the DirectoryEntry.RefreshCache() is going to translate to one single LDAP query. So, if you are trying to access 1000 objects, you are going to run 1000 different LDAP queries.
Take a relational database analogy, in VBscript, you are running
SELECT * FROM USER_TABLE
In C# code, you are running multiple times of the following queries
SELECT * FROM USER_TABLE WHERE id = #id
Of course, the C# code will be slower.
To do similar thing in C# code, you should use DirectorySearcher instead of DirectoryEntry.
Similarly, you need to remember to specify DirectorySearcher.PropertiesToLoad in order to specify what attributes to return from a LDAP query. If you don't specify, it will return all non-constructed attributes to you again.
Here are couple of things you can do
Enable Audit log in the LDAP server and see how your requests are going through. Audit logs will show you how much time it is taking for each request from your application and how many connections are opened etc.
Use System.DirectoryServices.Protocols which can make Asynchronous calls to LDAP. Check this sample post. Another advantage of using this name space is that you can specify attributes.
Close connection properly.
use DirectorySearcher.PropertiesToLoad to load only required properties instead of all properties.
what i see from vbscript following is a bit closer .. copying from some project, kindly test
DirectoryEntry de = new DirectoryEntry("ldap://domainname");
DirectorySearcher deSearch = new DirectorySearcher();
deSearch.SearchRoot = de;
deSearch.Filter = "(&(ObjectCategory=user)(sAMAccountName="+strUser+"))";
deSearch.PropertiesToLoad.Add("displayName");
deSearch.PropertiesToLoad.Add("pwdLastSet");
deSearch.PropertiesToLoad.Add("whenCreated");
deSearch.PropertiesToLoad.Add("whenChanged");
deSearch.PropertiesToLoad.Add("userAccountControl);
deSearch.SearchScope = SearchScope.Subtree;
SearchResult sr = deSearch.FindOne();
This is the correct way to read the property:
If searchResult.Properties.Contains(PropertyName) Then
Return searchResult.Properties(PropertyName)(0).ToString()
Else
Return String.Empty
End If
Related
I use Hard-Drive Serial-Number check to register my products.
But I see that sometimes it's being changed.
Just 2 weeks after the client has activated the product, the hard-drive Serial-Num changed.
Is this property changeable? and if so, how when and why is it changed?
Here is the code i use in VB:
Dim WMIService As Object, Items As Object, SubItems As Object, temp
Set WMIService = GetObject("winmgmts:\\" & "." & "\root\cimv2")
Set Items = WMIService.ExecQuery("Select * from Win32_PhysicalMedia", , 48)
For Each SubItems In Items
temp = SubItems.SerialNumber
If LenB(temp) Then Exit For
Next
and the same thing i use in C#
using System.Management;
public string GetHDDSerial()
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMedia");
foreach (ManagementObject wmi_HD in searcher.Get())
{
// get the hardware serial no.
if (wmi_HD["SerialNumber"] != null)
return wmi_HD["SerialNumber"].ToString();
}
return string.Empty;
}
Thanks to the comment from FunThomas and after a little troubleshooting with my client, i found out that he is using sometimes an external drive for more space, and when the external drive is plugged in - i'm getting a different HDD Serial-number.
Now i have figured how to get the serial number from the current drive letter.
here is the code for VB:
Dim WMIService As Object, DiskDrives As Object, Drive As Object, DiskPartitions As Object, Partition As Object, SerialNumber, currentDrive
currentDrive = Environ("HomeDrive")
Set WMIService = GetObject("winmgmts:\\" & "." & "\root\cimv2")
Set DiskPartitions = WMIService.ExecQuery("ASSOCIATORS OF {Win32_LogicalDisk.DeviceID='" & currentDrive & "'} WHERE ResultClass=Win32_DiskPartition")
For Each Partition In DiskPartitions
Set DiskDrives = WMIService.ExecQuery("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" & Partition.DeviceID & "'} WHERE ResultClass=Win32_DiskDrive")
For Each Drive In DiskDrives
Debug.Print "Caption: " & Drive.Caption, "Description: " & Drive.Description, "Name: " & Drive.Name, "Model: " & Drive.Model, "SerialNumber: " & Drive.SerialNumber
SerialNumber = Drive.SerialNumber
Next
Next
I'm trying to build a query, to list all the known computers in SCCM with a specific name.
The query looks like this:
string query = string.Format("Select Name From SMS_R_System Where Name like '" + "%" + computerName + "%" + "'");
If results are found, it puts the result(s) in a dropdown box.
My problem in these case, the output looks like this:
"instance of SMS_R_System{Name = "DC01";};"
But of course, for our use case we only need DC01 as output.
Any tips?
The full Code for the ButtonEvent:
private void ChkBtn_Click(object sender, EventArgs e)
{
string computerName = PCDropDown.Text;
lBox.Items.Clear();
SmsNamedValuesDictionary namedValues = new SmsNamedValuesDictionary();
WqlConnectionManager connection = new WqlConnectionManager(namedValues);
// Connect to remote computer.
try
{
connection.Connect(PrimarySiteServer.ToString());
// Set the query.
string query1 = string.Format("Select Name From SMS_R_System Where Name like '" + "%" + computerName + "%" + "'");
string query2 = string.Format("Select * From SMS_UserMachineRelationship WHERE ResourceName like '" + "%" + computerName + "%" + "' AND IsActive = '1' AND Types = '1'");
// Get the query results
IResultObject queryResults = connection.QueryProcessor.ExecuteQuery(query1);
// Check for results and display in infobox
bool resultsFound = false;
foreach (IResultObject queryResult in queryResults)
{
resultsFound = true;
lBox.Items.Add("Rechner ist vorhanden");
PCDropDown.Items.Add(queryResult.ToString());
}
if (resultsFound == false)
{
lBox.Items.Add("Rechnername nicht gefunden");
}
}
catch
{
MessageBox.Show("No Connection to Config-Manager - Als ZZA ausgeführt? SCCM-Servername richtig?");
}
}
Instead of adding queryResult.ToString() like you do here:
PCDropDown.Items.Add(queryResult.ToString());
you need to add the correct field of queryResult, so in this case:
PCDropDown.Items.Add(queryResult["Name"].StringValue);
Also a quick note. I don't know for who you are writing this and what the next step would be but if this is a read only application that is only used by SCCM Admins I would consider ignoring WMI and going to the SCCM DB via SQL instead. It is a lot faster, SQL has far more powerful options for queries and it does not need the integration of those strange sccm console Dlls (although that is not 100% necessary for WMI either).
If you need write access to create devices or collections etc., or you need to work with the roles the sccm access rights systems implements however WMI is the better or only choice. (And in this case I'd rather really use those strange dlls because all of the MS examples rely on them and it can be hard to translate those tutorials to the vanilla WMI solution C# offers.
I want ask you one problem that I have with one query to the System table.
This is the code that I have wrong.
string current_query = "*[(System[TimeCreated[#SystemTime > '" + dateini + "']]) and (System[TimeCreated[#SystemTime < '" + datenext + "']])]";
Console.WriteLine(current_query);
EventLogQuery eventsQuery = new EventLogQuery(log_location, PathType.FilePath, current_query);
EventLogReader logReader = new EventLogReader(eventsQuery);
The dateini and datenext are DateTime variables.I think that the problem is that I read the table two times. But I need read two times because I need obtain the logs of the System of one full day and I think I must compare between two System Times.
Any suggestion that how remake the consult.
UPDATE:
I try this query with the same result.
string current_query = "*[(Event/System/TimeCreated/#SystemTime > " + dateini + ") and (Event/System/TimeCreated/#SystemTime < " + datenext + ")]";
Console.WriteLine(current_query);
EventLogQuery eventsQuery = new EventLogQuery(log_location, PathType.FilePath, current_query);
EventLogReader logReader = new EventLogReader(eventsQuery);
Please I need help because I don't know which is the error in the Query.
TimeCreated is of the form:
<TimeCreated SystemTime="2013-08-15T16:21:21.000000000Z" />
is your datetime correct?
Shouldn't the query be more like:
Event/System/TimeCreated/#SystemTime
See MSDN and reading-event-logs-efficiently-using-c for some examples.
EDIT
Why are you using > and lt; ? Change lt; to <
This turned out to be the problem.
I've got an ASP.NET 4.0 C# web application that allows multiple users to update rows in the SQL Server DB at the same time. I'm trying to come up with a quick system that will stop USER1 from updating a row that USER2 updated since USER1's last page refresh.
The problem I'm having is that my web application always updates the row, even when I think it shouldn't. But when I manually run the query it only updates when I think it should.
This is my SQL query in C#:
SQLString = "update statuses set stat = '" + UpdaterDD.SelectedValue +
"', tester = '" + Session["Username"].ToString() +
"', timestamp_m = '" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") +
"' where id IN (" + IDs + ") and timestamp_m < '" + PageLoadTime + "';";
And here's a 'real world' example:
SQLString = "update statuses set stat = 'PASS', tester = 'tester007',
timestamp_m = '2013-01-23 14:20:07.221' where id IN (122645) and
timestamp_m < '2013-01-23 14:20:06.164';"
My idea was that this will only update if no other user has changed this row since the user last loaded the page. I have formatted PageLoadTime to the same formatting as my SQL Server DB, as you can see with DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), but something still isn't right.
Does anyone know why I get two different results? Is what I want to do even possible?
I really think the problem is that the page load time is not being set correctly, or is being set immediately before the DB call. For debugging, you may try hardcoding values into it that you know will allow or disallow the insert.
Here's a parameterized version of what you have. I also am letting the DB server set the timestamp to its current time instead of passing a value. If your DB server and your Web server may not have their time of day in synch, then set it yourself.
Using parameterization, you don't have to worry about whether the date/time format is correct or not. I don't know what the DB types are of stat, tester, and timestamp_m so adding the parameter DB type may need adjusting.
string sql = "update statuses set stat = #stat, tester = #tester" +
", timestamp_m = getdate()" +
" where id IN (" + IDs + ") and timestamp_m < #pageLoadTime";
SQLConnection conn = getMeASqlConnection();
SQLCommand cmd = new SQLCommand(sql, conn);
cmd.Parameters.Add("#stat", System.Data.SqlDbType.NVarChar).Value = UpdaterDD.SelectedValue;
cmd.Parameters.Add("#tester", System.Data.SqlDbType.NVarChar).Value = Session["Username"].ToString();
// Here, pageLoadTime is a DateTime object, not a string
cmd.Parameters.Add("#pageLoadTime", System.Data.SqlDbType.DateTime).Value = pageLoadTime;
The code below works just fine, however what's happening is the code limits the results to 1500 users and we have more than 1500 users. What I'm trying to do is retrieve a list of all users that are a member of a specific group. I know DirectorySearcher has a PageSize setting however, I'm unable to find a way to set DirectoryEntry PageSize will still only pulling members of that group.
Does anybody know a way to change the page size? Or maybe how to pull members of a specific group in another fashion that will accommodate pagesize?
DirectoryEntry dEntryhighlevel = new DirectoryEntry("LDAP://CN=Users,OU=MyOu,OU=Clients,OU=Home,DC=bridgeTech,DC=net");
foreach (object dn in dEntryhighlevel.Properties["member"])
{
DirectoryEntry singleEntry = new DirectoryEntry("LDAP://" + dn);
DirectorySearcher dSearcher = new DirectorySearcher(singleEntry);
//filter just user objects
dSearcher.SearchScope = SearchScope.Base;
//dSearcher.Filter = "(&(objectClass=user)(dn=" + dn + "))";
//dSearcher.PageSize = 1000;
SearchResult singleResult = null;
singleResult = dSearcher.FindOne();
if (singleResult != null)
{
string Last_Name = singleResult.Properties["sn"][0].ToString();
string First_Name = singleResult.Properties["givenname"][0].ToString();
string userName = singleResult.Properties["samAccountName"][0].ToString();
string Email_Address = singleResult.Properties["mail"][0].ToString();
OriginalList.Add(Last_Name + "|" + First_Name + "|" + userName + "|" + Email_Address);
}
singleEntry.Close();
}
This came up in another thread recently: Always getting 1500 member of distribution list using PowerShell
In short, you want to use ranged retrieval to get the membership. This is the mechanism designed to help you fetch large attributes with >1500 values in them.
While we're on this topic, I'd like to predict your next thread. :) Reading the membership of the group will yield missing results depending upon the API you use. If you are "close to the metal" and using LDAP APIs, you'll find that users in the group due to primary group membership will be missing. I'd test this with whatever approach you use after resolving the ranged retrieval issue to ensure you don't miss anyone.
More info on this here: retrieving group members/membership from active directory when members attrib doesn't work
I'm working on something similar to this at the moment and noticed that your code differs to mine slightly. I haven't had any issues with limited results using the following code structure:
DirectoryEntry dEntryhighlevel = new DirectoryEntry("LDAP://CN=Users,OU=MyOu,OU=Clients,OU=Home,DC=bridgeTech,DC=net");
DirectorySearcher dSearcher = new DirectorySearcher();
//filter just user objects
dSearcher.Filter = "(objectClass=user)";
dSearcher.PageSize = 1000;
SearchResultCollection resultCollection = dirSearcher.FindAll();
foreach (SearchResult userResults in resultCollection )
{
string Last_Name = userResults .Properties["sn"][0].ToString();
string First_Name = userResults .Properties["givenname"][0].ToString();
string userName = userResults .Properties["samAccountName"][0].ToString();
string Email_Address = userResults .Properties["mail"][0].ToString();
OriginalList.Add(Last_Name + "|" + First_Name + "|" + userName + "|" + Email_Address);
}
That should return all your users. You'll need to use LDAP search patterns in your dSearcher.Filter in order to narrow users down to a specific group - see this link for some additional help with that.