Working with .NET Objects & COM Directly

PowerShell Intermediate/Advanced

Chapter 5 · Working with .NET Objects & COM Directly

Every object this entire course has ever touched — a process, a file, an error record — has secretly been a .NET object the whole time, per Fundamentals 5's own opening finding. Every one of them arrived pre-built, courtesy of a cmdlet. This chapter is about building and calling into .NET directly, without a cmdlet doing it for you first — plus its considerably older cousin, COM, which comes with a real, painful cleanup responsibility no other chapter in this course has needed to mention.

Creating Objects: New-Object vs. [Type]::new()

# Older syntax — works everywhere, more verbose $sb1 = New-Object System.Text.StringBuilder # Newer syntax (PowerShell 5+) — generally preferred, skips New-Object's own parameter-binding overhead $sb2 = [System.Text.StringBuilder]::new()

Both lines create the identical type of object — [Type]::new() is simply the more direct, modern way to say the same thing.

Static vs. Instance Members

# Static — called on the TYPE itself, no object ever created [Math]::Pow(2, 10) # 1024 [Math]::PI # 3.14159265358979 # Instance — called on a particular OBJECT you actually created $sb2.Append("Hello") $sb2.ToString()

[Math] exposes only static members — there's no such thing as "an instance of Math," which is why you'll never see New-Object Math or [Math]::new() anywhere. The [Type]::Member syntax always means "call this directly on the type"; a variable's own .Member always means "call this on the specific object that variable holds."

A Real Payoff: StringBuilder vs. String Concatenation at Scale

# Inefficient at scale — strings are immutable in .NET $result = "" foreach ($i in 1..10000) { $result += "line $i`n" } # Efficient — StringBuilder mutates in place instead of copying $sb = [System.Text.StringBuilder]::new() foreach ($i in 1..10000) { [void]$sb.AppendLine("line $i") } $result = $sb.ToString()

Every += against a string doesn't modify it in place — it can't, because .NET strings are immutable. Each one silently builds an entirely new string containing a full copy of everything accumulated so far, plus the new piece. At 10,000 iterations that's not 10,000 small operations; it's roughly 10,000 progressively larger copies, real quadratic-ish cost that gets noticeably slow well before the loop ends. StringBuilder avoids all of that by mutating an internal buffer directly. The [void] cast on AppendLine isn't decoration — Append/AppendLine both return the StringBuilder itself (for chaining), and per Fundamentals 7's own central rule, an uncaptured return value would otherwise silently join the loop's own output.

Other Useful .NET Types

[System.IO.Path]::Combine("C:\Users\Philip", "Documents", "report.txt") # C:\Users\Philip\Documents\report.txt — correct separators, no manual string-joining needed [System.IO.Path]::GetExtension("report.txt") # .txt [System.IO.Path]::GetFileNameWithoutExtension("report.txt") # report

[System.IO.Path] is generally more robust than hand-built string concatenation for paths — it handles separator characters correctly regardless of trailing slashes on the inputs. [regex], already covered in Chapter 2, is the same idea applied to pattern matching — both are ordinary .NET types, reached exactly the way [Math] and [System.Text.StringBuilder] are here.

COM Objects: New-Object -ComObject

COM (Component Object Model) predates .NET entirely, and it isn't part of the managed object model this chapter has covered so far — New-Object -ComObject is the only way in; there's no [Type]::new() equivalent, because a COM class isn't a .NET type in the first place.

$excel = New-Object -ComObject Excel.Application $excel.Visible = $true $workbook = $excel.Workbooks.Add() $sheet = $workbook.Worksheets.Item(1) $sheet.Cells.Item(1, 1) = "Hello from PowerShell" $workbook.SaveAs("C:\temp\report.xlsx") $excel.Quit()

The COM Cleanup Gotcha: Ghost Processes

Skipping .Quit() can leave EXCEL.EXE running invisibly, indefinitely
A managed .NET object's memory is cleaned up automatically by the garbage collector once nothing references it anymore — every object this course has used so far worked this way with no extra effort. A COM object doesn't get that same reliable, prompt cleanup: the underlying native application (EXCEL.EXE, here) can keep running as an orphaned background process even after $excel goes out of scope and the script finishes. Run a script like this repeatedly without .Quit(), and Task Manager quietly accumulates one ghost EXCEL.EXE process per run — invisible (since Visible only controlled the window, not the underlying process's lifetime the moment it's abandoned), silently consuming memory, never asked for.
$excel.Quit() [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null Remove-Variable excel
The central fact this chapter is built on
Every .NET object covered earlier in this chapter cleans itself up automatically the moment nothing references it — that's what "managed" means. COM sits entirely outside that system: it's your own explicit responsibility to call .Quit() (or the more general Marshal.ReleaseComObject()) every time, on every COM object you create, with no automatic safety net behind you. Skipping it doesn't error, doesn't warn, and doesn't fail the script — it just quietly leaves something running that should have stopped.
A first practical habit
Wrap any COM automation in try/finally (Fundamentals 9) with the cleanup calls inside finally — that guarantees .Quit() runs even if something in the middle of the script throws, which is exactly the situation most likely to leave a ghost process behind in practice.

Hands-On Exercises

Exercise 1

Explain the real performance difference between building a 10,000-line string with += in a loop versus using StringBuilder, using this chapter's own explanation of string immutability.

📄 View solution
Exercise 2

Explain why [Math]::Pow(2, 10) works with no object creation step first, while $sb.Append("x") requires $sb to already exist as a real, created object. What's the underlying distinction?

📄 View solution
Exercise 3

Explain why forgetting to call $excel.Quit() after New-Object -ComObject Excel.Application can leave EXCEL.EXE running in the background indefinitely, even after the script finishes and $excel goes out of scope.

📄 View solution

Chapter 5 Quick Reference

  • New-Object TypeName / [Type]::new() — two ways to create a .NET object; [Type]::new() is the newer, generally preferred syntax
  • Static members ([Type]::Member) — called on the type itself, no object required; e.g. [Math]::Pow(), [Math]::PI
  • Instance members ($object.Member) — called on a specific, already-created object
  • StringBuilder — mutates a buffer in place; far cheaper than repeated += on an immutable string at scale
  • [System.IO.Path]::Combine() — robust path-joining, correct separators regardless of trailing slashes
  • New-Object -ComObject — the only way to create a COM object; no [Type]::new() equivalent exists for COM
  • COM cleanup is manual.Quit() and/or Marshal.ReleaseComObject(); skipping it can leave a ghost process (e.g. EXCEL.EXE) running indefinitely, with no automatic garbage collection to rescue you
  • Next chapter: CIM/WMI: Querying and Managing Windows Systems