I have an application I made with PowerShell that obtains info from a SQL database for various controls. My 'View', will occasionally work perfectly. But if I close the application and reopen, the same thing can yield different results. I've set PS to run as admin, but again the same issue. The view should read 1971 results and look like
#ASP.Net Client Ugrade
C# Client Upgrade
Another Basic Project Name
However, while this works correctly some of the time...the output I get when it decides to not load correctly is
#ASP.Net Client Ugrade
1971
void Open(), void IDbConne
I'm not sure why it's adding property information instead of the values being requested. Also not sure why the results sometimes are right and sometimes wrong, while doing the same behavior(Just starting the program, the box is populated on load). Relevant code to how I'm selecting and populating the data:
[string[]]$projectsAll = "" #I only do it this way because the sql query is actually in a method, were I return the array-and then do the foreach item in the array add to the projectview
$query = "select Title from OversightProjectsFix where ID > 0 Order By Title"
if ($sqlcon.State -eq 'Closed'){$sqlcon.Open()}
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $query
$SqlCmd.Connection = $sqlcon
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$ds = New-Object System.Data.DataSet
$SqlAdapter.Fill($ds)|out-null
foreach ($Row in $ds.Tables[0].Rows)
{
$projectsAll += "$($Row.Title)"
}
$ProjectView.Items.Clear()
foreach ($p in $projectsAll)
{
$ProjectView.Items.Add($p)
}
As I mentioned before, if this same code is ran, you should always get the same results-but this one returns differently and I'm not sure why? How can I make it always work?
It's hard to understand exactly what is wrong with this snippit of code, but I think that it may be because the DataSet is empty, and you are . i.e. I think that it isn't connecting and pulling any data.
Since the dataset is initialized with New-Object, if it didn't fill properly, what happens is if you try to iterating through it using quotes, instead of adding it as a straight assignment, it may cause it to unexpectedly return the data type instead of an "expected" null or empty string. You can see this when you try something like this:
$ds = New-Object System.Data.DataSet
#Returns Null
PS> $ds.Tables[0].Rows
PS>
#Returns data type
PS> "$ds.Tables[0].Rows"
System.Data.DataSet.Tables[0].Rows
So you may have to remove the quotes i.e.:
$projectsAll += $Row.Title
You should add a check to see if the dataset has something before trying to iterate and add anything.
Also, usually by implementing the $SqlAdapter.Fill($ds) method opens the connection, without you needing to explicitly open the SQL connection. I wonder if your inconsistencies is due to the SQL connection opening/closing unexpectedly?
Related
So I received a list of users from a co-worker who needed to confirm who in the list was still employed and who wasn't. I chose to filter out all users that either didn't exist in AD or were disabled and assign them to $TerminatedUser. I took all active users that assigned them to $EmployeedUser. (I know I spelled "Employed" wrong) I then tried to use the data from $EmployeedUser and $TerminatedUser and create a report within $EmployementStatus.
What I end up with is two columns which is awesome but I also only get 1 cell for each column. All the data for each column is bunched into one cell which makes it hard to read. At first when outputting $EmployementStatus to a csv file was only getting the headers and [system.object] for each cell. I was able to get around that.
So my question here now is: Is it possible to export $EmployementStatus to a csv where the data is listed out and each "Employed"/"Terminated" user receives their own cell as opposed to them all being bunched in cells A2 and B2?
Teach me something!
This is sample code, since I'm not going to type out all that stuff again. And it isn't tested.
What you want, apparently, is to check there's an enabled AD user account that matches your userlist. For Powershell versions greater than 3.0, you can output [pscustomobject] directly into an array from a Foreach.
You just need ONE query to AD to determine if a user exists and whether the account is enabled ("Enabled" is one of the default properties returned in Get-AdUser).
It's probably more convenient for output if you simply have a "Verified" column and set that to TRUE or FALSE. Or you can have a "Status" column and output text to that like "Disabled" or "NotPresent" or "Verified". Whatever, really, I'm going with the easiest.
The try/catch is so you don't get a load of errors when the user doesn't exist. If you want to set different statuses for each "state", then you can place strings in there rather than $true/$false.
$employmentStatus = Foreach ($GID in $MyList) {
$ID = $GID.SamAccountname
try {
# if the user isn't found, it'll go to the Catch block after the next line
$u = get-aduser $ID -erroraction stop
if ($u.enabled) {
$verified = $true
}
else {
$verified = $false
}
}
catch {
# if the user doesn't exist, they're not verified
$verified = $false
}
# output the per-user status as a pscustomobject in $employmentStatus
[pscustomobject]#{
ADUser = $ID
Verified = $verified
}
}
You should find that if you process your userlist with that, you can check the result with $employmentStatus | out-gridview.
That should show the "AdUser" and "Verified" columns, with TRUE or FALSE for each user.
If that looks OK, so will your CSV export: $employmentStatus | export-csv [path].
If you're using an old PS version, then you may need to predefine your output array as you did originally. Then you'd just fix up the line with the [pscustomobject] to append it to the array. Everything else works the same.
$employmentStatus = #()
Foreach ($GID in $MyList) {
...
# output the per-user status as a pscustomobject - append to $employmentStatus
$employmentStatus += [pscustomobject]#{
ADUser = $ID
Verified = $verified
}
}
I have a form in which as soon as ready several elements will be added (for example, a list). It may take some time to add them (from fractions of a second to several minutes). Therefore, I want to add processing to a separate thread (child). The number of elements is not known in advance (for example, how many files are in the folder), so they are created in the child stream. When the processing in the child stream ends, I want to display these elements on the main form (before that the form did not have these elements and performed other tasks).
However, I am faced with the fact that I cannot add these elements to the main form from the child stream. I will give a simple example as an example. It certainly works:
$Main = New-Object System.Windows.Forms.Form
$Run = {
# The form is busy while adding elements (buttons here)
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
}
$Main.Add_Shown($Run)
# Adding and performing other tasks on the form here
[void]$Main.ShowDialog()
But, adding the same thing to the child stream I did not get the button to display on the main form. I do not understand why.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
# Many items will be added here. Their number and processing time are unknown in advance
# Now an example with the addition of five buttons.
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
})
$PS.Runspace = $RS; $Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
How can I add elements to the main form that are created in the child stream? thanks
While you can create controls on thread B, you cannot add them to a control that was created in thread A from thread B.
If you attempt that, you'll get the following exception:
Controls created on one thread cannot be parented to a control on a different thread.
Parenting to means calling the .Add() or .AddRange() method on a control (form) to add other controls as child controls.
In other words: In order to add controls to your $Main form, which is created and later displayed in the original thread (PowerShell runspace), the $Main.Controls.Add() call must occur in that same thread.
Similarly, you should always attach event delegates (event-handler script blocks) in that same thread too.
While your own answer attempts to ensure adding the buttons to the form in the original runspace, it doesn't work as written - see the bottom section.
I suggest a simpler approach:
Use a thread job to create the controls in the background, via Start-ThreadJob.
Start-ThreadJob is part of the the ThreadJob module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
Show your form non-modally (.Show() rather than .ShowDialog()) and process GUI events in a [System.Windows.Forms.Application]::DoEvents() loop.
Note: [System.Windows.Forms.Application]::DoEvents() can be problematic in general (it is essentially what the blocking .ShowDialog() call does behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.
In the loop, check for newly created buttons as output by the thread job, attach an event handler, and add them to your form.
Here is a working example that adds 3 buttons to the form after making it visible, one after the other while sleeping in between:
Add-Type -ea Stop -Assembly System.Windows.Forms
$Main = New-Object System.Windows.Forms.Form
# Start a thread job that will create the buttons.
$job = Start-ThreadJob {
$top = 0
1..3 | % {
# Create and output a button object.
($btn = [System.Windows.Forms.Button] #{
Name = "Button$_"
Text = "Button$_"
Top = $top
})
Start-Sleep 1
$top += $btn.Height
}
}
# Show the form asynchronously
$Main.Show()
# Process GUI events in a loop, and add
# buttons to the form as they're being created
# by the thread job.
while ($Main.Visible) {
[System.Windows.Forms.Application]::DoEvents()
if ($button = Receive-Job -Job $job) {
# Add an event handler...
$button.add_Click({ Write-Host "Button clicked: $($this.Name)" })
# .. and it to the form.
$Main.Controls.AddRange($button)
}
}
# Clean up.
$Main.Dispose()
Remove-Job -Job $job -Force
'Done'
As of this writing, your own answer tries to achieve adding the controls to the form in the original runspace by using Register-ObjectEvent to subscribe to the other thread's (runspace's) events, given that the -Action script block used for event handling runs (in a dynamic module inside) the original thread (runspace), but there are two problems with that:
Unlike your answer suggests, the -Action script block neither directly sees the $Main variable from the original runspace, nor the other runspace's variables - these problems can be overcome, however, by passing $Main to Register-ObjectEvent via -MessageData and accessing it via $Event.MessageData in the script block, and by accessing the other runspace's variables via $Sender.Runspace.SessionStateProxy.GetVariable() calls.
More importantly, however, the .ShowDialog() call will block further processing; that is, your events won't fire and therefore your -Action script block won't be invoked until after the form closes.
Update: You mention a workaround in order to get PowerShell's events to fire while the form is being displayed:
Subscribe to the MouseMove event with a dummy event handler whose invocation gives PowerShell a chance to fire its own events while the form is being displayed modally; e.g.: $Main.Add_MouseMove({ Out-Host }); note that this workaround is only effective if the script block
calls a command, such as Out-Host in this example (which is effectively a no-op); a mere expression or .NET method call is not enough.
However, this workaround is suboptimal in that it relies on the user (continually) mousing over the form for the PowerShell events to fire; also, it is somewhat obscure and inefficient.
I think you can't create form and controls on different threads. But you can access control properties though. So you can create form with control placeholders in a runspace, then change them on the main thread once your calculations are complete. Example:
$form = New-Object System.Windows.Forms.Form
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = [System.Threading.ApartmentState]::MTA
$ps = [powershell]::create()
$ps.Runspace = $rs
$rs.Open()
$out = $ps.AddScript({param($form)
$button1 = New-Object System.Windows.Forms.Button
$button1.Name = "button1"
$form.Controls.Add($button1)
$form.ShowDialog()
}).AddArgument($form).BeginInvoke()
#-----------------------------
sleep 1;
$form.Controls["button1"].Text = "some button"
This is the way I'm using now. Thanks to #mklement0 for the talk about the Register-ObjectEvent method (here). I applied it here. The essence of the method is that the elements are created in the child stream (in this case, the Button), and when the child space has finished work, Register-ObjectEvent is processed. Register-ObjectEvent is located in the main space and therefore allows you to add an element (Button here) to the form.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
$Button = New-Object System.Windows.Forms.Button
}
})
$PS.Runspace = $RS
$Null = Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action {
if ($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed') {
$Main.Controls.Add($Button)
}
}
$Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
This is my a workaround. However, I still do not know if it is possible in principle to add elements of a child space to a form from a child space. This, of course, is about adding, not managing, because managing from the child space is successful.
Putting this here, since it is too long for the regular comment section.
Now, I don't spend much time using runspaces in production, as I've no had a real need (at least to date) for them, in class, sure, but I digress.
However, from all my previous readings and notes I've kept, this sounds like a use case for RunSpace Pools. Here are three of my saved resources. Working under the assumption that you may have not seen all of them of course. Now, I would post their code as well, but all are very long, so, there's that. Based on your use case, it could be seen as a duplicate to the last link resource.
PowerShell and WPF: Writing Data to a UI From a Different
Runspace
PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI
Applications
Sharing Variables and Live Objects Between PowerShell Runspaces
How to access a different powershell runspace without WPF-object
I'm trying to execute the Invoke-Sqlcmd command (from the SqlServer module) to run a query as a different AD user. I know there's the -Credential argument, but that doesn't seem to work.
Thus, I thought using Start-Job might be an option, as shown in the snippet below.
$username = 'dummy_domain\dummy_user'
$userpassword = 'dummy_pwd' | ConvertTo-SecureString -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential ($username, $password)
$job = Start-Job -ScriptBlock {Import-Module SqlServer; Invoke-Sqlcmd -query "exec sp_who" -ServerInstance 'dummy_mssql_server' -As DataSet} -Credential $credential
$data = Receive-Job -Job $job -Wait -AutoRemoveJob
However, when looking at the variable type that the job returned, it isn't what I expected.
> $data.GetType().FullName
System.Management.Automation.PSObject
> $data.Tables[0].GetType().FullName
System.Collections.ArrayList
If I run the code in the ScriptBlock directly, these are the variable types that PS returns:
> $data.GetType().FullName
System.Data.DataSet
> $data.Tables[0].GetType().FullName
System.Data.DataTable
I tried casting the $data variable to [System.Data.DataSet], which resulted in the following error message:
Cannot convert value "System.Data.DataSet" to type "System.Data.DataSet".
Error: "Cannot convert the "System.Data.DataSet" value of type
"Deserialized.System.Data.DataSet" to type "System.Data.DataSet"."
Questions:
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Update
When I run $data.Tables | Get-Member, one of the properties returned is:
Tables Property Deserialized.System.Data.DataTableCollection {get;set;}
Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Due to using a background job, you lose type fidelity: the objects you're getting back are method-less emulations of the original types.
Manually recreating the original types is not worth the effort and may not even be possible - though perhaps working with the emulations is enough.
Update: As per your own answer, switching from working with System.DataSet to System.DataTable resulted in serviceable emulations for you.[1]
See the bottom section for more information.
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
You need an in-process invocation method in order to maintain type fidelity, but I don't think that is possible with arbitrary commands if you want to impersonate another user.
For instance, the in-process (thread-based) alternative to Start-Job - Start-ThreadJob - doesn't have a -Credential parameter.
Your best bet is therefore to try to make Invoke-SqlCmd's -Credential parameter work for you or find a different in-process way of running your queries with a given user's credentials.
Serialization and deserialization of objects in background jobs / remoting / mini-shells:
Whenever PowerShell marshals objects across process boundaries, it employs XML-based serialization at the source, and deserialization at the destination, using a format known as CLI XML (Common Language Infrastructure XML).
This happens in the context of PowerShell remoting (e.g., Invoke-Command calls with the
-ComputerName parameter) as well as in background jobs (Start-Job) and so-called mini-shells (which are implicitly used when you call the PowerShell CLI from inside PowerShell itself with a script block; e.g., powershell.exe { Get-Item / }).
This deserialization maintains type fidelity only for a limited set of known types, as specified in MS-PSRP, the PowerShell Remoting Protocol Specification. That is, only instances of a fixed set of types are deserialized as their original type.
Instances of all other types are emulated: list-like types become [System.Collections.ArrayList] instances, dictionary types become [hasthable] instances, and other types become method-less (properties-only) custom objects ([pscustomobject] instances), whose .pstypenames property contains the original type name prefixed with Deserialized. (e.g., Deserialized.System.Data.DataTable), as well as the equally prefixed names of the type's base types (inheritance hierarchy).
Additionally, the recursion depth for object graphs of non-[pscustomobject] instances is limited to 1 level - note that this includes instance of PowerShell custom classes, created with the class keyword: That is, if an input object's property values aren't instance of well-known types themselves (the latter includes single-value-only types, including .NET primitive types such as [int], as opposed to types composed of multiple properties), they are replaced by their .ToString() representations (e.g., type System.IO.DirectoryInfo has a .Parent property that is another System.IO.DirectoryInfo instance, which means that the .Parent property value serializes as the .ToString() representation of that instance, which is its full path string); in short: Non-custom (scalar) objects serialize such that property values that aren't themselves instances of well-known types are replaced by their .ToString() representation; see this answer for a concrete example.
By contrast, explicit use of CLI XML serialization via Export-Clixml defaults to a depth of 2 (you can specify a custom depth via -Depth and you can similarly control the depth if you use the underlying System.Management.Automation.PSSerializer type directly).
Depending on the original type, you may be able to reconstruct instances of the original type manually, but that is not guaranteed.
(You can get the original type's full name by calling .pstypenames[0] -replace '^Deserialized\.' on a given custom object.)
Depending on your processing needs, however, the emulations of the original objects may be sufficient.
[1] Using System.DataTable results in usable emulated objects, because you get a System.Collections.ArrayList instance that emulates the table, and custom objects with the original property values for its System.DataRow instances. The reason this works is that PowerShell has built-in logic to treat System.DataTable implicitly as an array of its data rows, whereas the same doesn't apply to System.DataSet.
I can't say for question 2 as I've never used the job commands but when it comes to running the Invoke-Sqlcmd I always make sure that the account that runs the script has the correct access to run the SQL.
The plus to this is that you don't need to store the credentials inside the script, but is usually a moot point as the scripts are stored out of reach of most folks, although some bosses can be nit picky!
Out of curiosity how do the results compare if you pipe them to Get-Member?
For those interested, below is the code I implemented. Depending on whether or not $credential is passed, Invoke-Sqlcmd will either run directly, or using a background job.
I had to use -As DataTables instead of -As DataSet, as the latter seems to have issues with serialisation/deserialisation (see accepted answer for more info).
function Exec-SQL($server, $database, $query, $credential) {
$sqlData = #()
$scriptBlock = {
Param($params)
Import-Module SqlServer
return Invoke-Sqlcmd -ServerInstance $params.server -Database $params.database -query $params.query -As DataTables -OutputSqlErrors $true
}
if ($PSBoundParameters.ContainsKey("credential")) {
$job = Start-Job -ScriptBlock $scriptBlock -Credential $credential -ArgumentList $PSBoundParameters
$sqlData = Receive-Job -Job $job -Wait -AutoRemoveJob
} else {
$sqlData = & $scriptBlock -params $PSBoundParameters
}
return $sqlData
}
I've searched all over the web and am unable to find a solution/guide for my problem.
I'm using the below bit of script its part of a larger script to export multiple SQL tables into CSVs. It fills a dataset with data from an SQL table.
The problem I have is in mainly in relation to datetime settings. For example, If I were to export the SQL table using the export wizard into a CSV file. The date appears exactly like it does in SQL e.g. "2014-05-23 07:00:00.0000000" or "2014-05-23".
However when I use my script it changes the format of the datetime to "23/05/2014 07:00:00" or "23/05/2014 00:00:00". I believe this has something to do with the culture settings of my machine/powershell session.
cls
# Declare variables for connection and table to export
$Server = 'server'
$Database = 'database'
$Folder = 'D:\Powershell Scripts\01_Export From SQL\Test Folder'
$Tablename1 = 'test'
$Tablename2 = ''
# Delcare Connection Variables
$SQLconnection = New-Object System.Data.SqlClient.SqlConnection
$SQLconnection.ConnectionString = "Integrated Security=SSPI;server=$Server;Database=$Database"
# Delcare SQL command variables
$SQLcommand = New-Object System.Data.SqlClient.SqlCommand
$SQLcommand.CommandText = "SELECT [name] from sys.tables where [name] like '%$Tablename1%' and [name] like '%$Tablename2%' order by [name]"
$SQLcommand.Connection = $SQLconnection
# Load up the Tables in a dataset
$SQLAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SQLAdapter.SelectCommand = $SQLcommand
$DataSet = New-Object System.Data.DataSet
$null = $SqlAdapter.Fill($DataSet)
$SQLconnection.Close()
"Time to Export`tRecords `tFile Name"
"--------------`t------- `t---------"
foreach ($Table in $DataSet.Tables[0])
{
$stopwatch = [system.diagnostics.stopwatch]::StartNew()
$FileExtractUTF8 = "$Folder\FromPSUTF8_$($Table[0]).csv"
$SQLcommand.CommandText = "SELECT * FROM [$($Table[0])]"
$SQLAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SQLAdapter.SelectCommand = $SQLcommand
$DataSet = New-Object System.Data.DataSet
$Count = $SQLAdapter.Fill($DataSet)
$SQLconnection.Close()
$DataSet.Tables[0] | Export-Csv $FileExtractUTF8 -NoTypeInformation -Encoding UTF8
$stopwatch.stop()
$Time = "{0}" -f $stopwatch.Elapsed.ToString('mm\:ss\.fff')
“{0,-14}`t{1,-10}`t{2}” -f $Time,$Count,$Table.name
}
The main goal is to export the data from SQL into a flat file with the data appearing exactly as it would if I used the export wizard.
Changing default DateTime format within a script
Within your DataSet / DataTable, the date exists as a [DateTime] type. When you export to CSV it needs to be converted to a string so it can be written to file. As you have observed, the default string conversion gives an output such as:
[PS]> (get-date).ToString()
21/03/2017 17:52:26
The format of this string is specified (as you worked out) by your specific globalization "culture". It appears to be a concatenation of the ShortDatePattern and LongTimePattern properties of the DateTimeFormat.
There are plenty of posts from people who have tried and failed to change the culture of their current session (i.e. the running PS host)...
why-does-powershell-always-use-us-culture-when-casting-to-datetime
...but it may be possible to change the globalization culture within your script using a mechanism such as the ones described here:
powershell-changing-the-culture-of-current-session
Using-Culture function
I suspect you should be able to use the Using-Culture example to wrap the Export-Csv line in your script.
But what culture to use?
So, you might now be able to set a specific culture, but do any of the pre-existing cultures use ISO8601 (sortable) dates? Or more specific formats? It seems not, so you have to make your own!
In short, you need to clone an existing CultureInfo and update the DateTimeFormat, specifically (at least) the ShortDatePattern to be what you want. Here is an example that I hope puts you on the right path. There are two functions, one which clones an existing CultureInfo and one which runs a command (Get-Date) with the new culture set for that thread.
function New-CultureInfo {
[CmdletBinding()]
[OutputType([System.Globalization.CultureInfo])]
param(
[Parameter()]
[System.Globalization.CultureInfo]
$BaseCulture = [System.Globalization.CultureInfo]::InvariantCulture
)
$CultureInfo = ($BaseCulture).Clone()
$CultureInfo.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd"
$CultureInfo.DateTimeFormat.LongTimePattern = "HH:mm:ss.fff"
return $CultureInfo
}
function Test-DateFormat {
[CmdletBinding()]
[OutputType([System.DateTime])]
param(
[Parameter(Mandatory)]
[System.Globalization.CultureInfo]
$Culture
)
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $Culture
[System.Threading.Thread]::CurrentThread.CurrentCulture = $Culture
(Get-Date).ToString()
}
Example use:
[PS]> $NewCulture = (New-CultureInfo)
[PS]> Test-DateFormat $NewCulture
2017-03-21 19:08:34.691
Now, I haven't been able to run this against an example close to the SQL problem in the OP, but I've had fun working this all out. ;-)
Great description by Charlie. My problem is that I wanted to change the default ToString to output an ISO datetime that contains a T separator between date and time instead of a space. TL;DR - it's not possible.
I'm more from the Java world than MS but had to write a script to export db CSVs and here's my investigation in case anyone else is interested in how the format is built. I dug into the source code to see how ToString works on DateTime.
According to the DateTime class it will defer to DateTimeFormat
https://referencesource.microsoft.com/#mscorlib/system/datetime.cs,0a4888bea7300518
public override String ToString() {
Contract.Ensures(Contract.Result<String>() != null);
return DateTimeFormat.Format(this, null, DateTimeFormatInfo.CurrentInfo);
}
This will eventually call into Format method with a null String format. This causes the block below to be called which basically specifies the "G" format to use for the date/time.
https://referencesource.microsoft.com/#mscorlib/system/globalization/datetimeformat.cs,386784dd90f395bd
if (offset == NullOffset) {
// Default DateTime.ToString case.
if (timeOnlySpecialCase) {
format = "s";
}
else {
format = "G";
}
}
This will eventually make a call to get the current Culture's DateTimeInfo object that has an internal String pattern that cannot be overridden. It lazily sets this so that it doesn't have to concatenate and there is no way to override it. As Charlie pointed out, it always concatenates ShortDatePattern and LongTimePattern with a space separator.
https://referencesource.microsoft.com/#mscorlib/system/globalization/datetimeformatinfo.cs,799bd6e7997c4e78
internal String GeneralLongTimePattern {
get {
if (generalLongTimePattern == null) {
generalLongTimePattern = ShortDatePattern + " " + LongTimePattern;
}
return (generalLongTimePattern);
}
}
So there you have it. Hope it helps the next person know why/how and that it's not possible to override the overall default format - just the date and the time components.
The ADSI query works fine, it returns multiple users.
I want to select the 'name' and 'email' from each object that is returned.
$objSearcher = [adsisearcher] "()"
$objSearcher.searchRoot = [adsi]"LDAP://dc=admin,dc=domain,dc=co,dc=uk"
$objSearcher.Filter = "(sn=Smith)"
$ADSearchResults = $objSearcher.FindAll()
$SelectedValues = $ADSearchResults | ForEach-Object { $_.properties | Select -property mail, name }
$ADSearchResults.properties.mail gives me the email address
When I omit the 'select -properties' it will return all the properties, but trying to select certain properties comes back with nothing but empty values.
Whenever working with ADSI I find it easier to expand the objects returned using .GetDirectoryEntry()
$ADSearchResults.GetDirectoryEntry() | ForEach-Object{
$_.Name
$_.Mail
}
Note: that doing it this way gives you access to the actual object. So it is possible to change these values and complete the changes with something like $_.SetInfo(). That was meant to be a warning but would not cause issues simply reading values.
Heed the comment from Bacon Bits as well from his removed answer. You should use Get-Aduser if it is available and you are using Active Directory.
Update from comments
Part of the issue is that all of these properties are not string but System.DirectoryServices.PropertyValueCollections. We need to get that data out into a custom object maybe? Lets have a try with this.
$SelectedValues = $ADSearchResults.GetDirectoryEntry() | ForEach-Object{
New-Object -TypeName PSCustomObject -Property #{
Name = $_.Name.ToString()
Mail = $_.Mail.ToString()
}
}
This simple approach uses each objects toString() method to break the data out of the object. Note that while this works for these properties be careful using if for other and it might not display the correct results. Experiment and Debug!
Have you tried adding the properties?
$objSearcher.PropertiesToLoad.Add("mail")
$objSearcher.PropertiesToLoad.Add("name")