Cómo invocar COM y .NET desde PowerShell en la práctica ── ampliar de un salto el alcance de sus scripts

· Actualizado el: · · PowerShell, Windows, COM, .NET, Automatización, Script, Aprovechamiento de activos existentes, Eficiencia operativa

«Busqué un cmdlet, pero no encontré el que hace lo que necesito» ── en cuanto se usa PowerShell con cierta profundidad, siempre se llega a esta pared. Crear accesos directos en lote, revisar el contenido de un ZIP sin descomprimirlo, manipular ventanas, leer y escribir archivos de Excel. Los responsables de sistemas informáticos y de operaciones de pequeñas y medianas empresas nos consultan a diario sobre necesidades que los cmdlets estándar por sí solos no alcanzan a cubrir.

En realidad, al otro lado de esa pared está el terreno en el que PowerShell brilla. Como PowerShell está construido sobre .NET, puede invocar directamente los tipos de la biblioteca de clases .NET sin instalación adicional. Además, con Add-Type puede incorporar en el momento código C# o la API Win32 (P/Invoke), y con New-Object -ComObject puede operar objetos COM como WScript.Shell o Excel. En otras palabras, la caja de herramientas de «automatización de Windows» que tradicionalmente se escribía en VBScript o VBA sigue estando disponible, prácticamente intacta.

Ahora bien, este poder viene acompañado de un protocolo de limpieza. En particular, la operación de Excel por COM es el caldo de cultivo clásico del problema «el script termina pero EXCEL.EXE sigue en ejecución», y Microsoft directamente desaconseja automatizar Office en ejecuciones desatendidas. En este artículo repasamos los patrones prácticos y las trampas de la invocación de .NET, Add-Type y la operación COM desde PowerShell, y ordenamos también el criterio de «hasta dónde conviene insistir con el script y a partir de dónde conviene convertirlo en una herramienta en C#».

1. Conclusión primero

  • PowerShell está construido sobre .NET. Windows PowerShell 5.1 está construido sobre .NET Framework y PowerShell 7 sobre .NET (antes .NET Core), y puede invocar directamente métodos estáticos de .NET como [System.IO.Path]::GetFileNameWithoutExtension(). 1
  • Para crear instancias se usa New-Object o, desde PowerShell 5.0, [Tipo]::new(). Si escribe [Tipo]::new (sin paréntesis) puede consultar la lista de constructores y escribir el código mientras verifica los argumentos. 2
  • Con Add-Type puede compilar código fuente de C# en el momento e incorporarlo a la sesión. Si le pasa una firma con DllImport también puede realizar llamadas P/Invoke a la API Win32. El tipo agregado no se puede eliminar dentro de la sesión, y no se puede volver a definir un tipo con el mismo nombre. 3
  • Los objetos COM se crean con New-Object -ComObject <ProgId>. El CreateObject("Shell.Application") de VBScript corresponde directamente a New-Object -ComObject Shell.Application, de modo que herramientas de la era WSH, como la creación de accesos directos con WScript.Shell, siguen disponibles desde PowerShell. 45
  • El ciclo de vida de un objeto COM se gestiona por conteo de referencias, y del lado de .NET es el RCW (Runtime Callable Wrapper) el que lo retiene. Si queda alguna referencia viva, procesos como Excel no terminarán. Para liberarlo explícitamente se usa Marshal.ReleaseComObject, pero abusar de él provoca otro tipo de incidentes, por lo que la guía oficial es usarlo «solo cuando sea realmente necesario». 67
  • Microsoft declara explícitamente que «no recomienda ni admite» la automatización de Office en ejecuciones desatendidas. Operar Excel por COM desde un servicio, el Programador de tareas o el lado del servidor es una configuración no admitida, aunque parezca funcionar. Los procesos desatendidos deben reemplazarse por bibliotecas de la familia Open XML o por integración con CSV. 8
  • Entre 5.1 y 7 difieren los tipos y métodos disponibles. Hay casos, como la diferencia de sobrecargas de String.Split, en los que el mismo código cambia de comportamiento, y en 5.1 hay situaciones en las que es necesario cargar explícitamente los ensamblados de la GAC con Add-Type. 13
  • Cuando las llamadas directas a .NET empiezan a multiplicarse, es la línea para plantearse convertir el script en una herramienta en C#. Si empiezan a destacar la restricción de Add-Type de no poder redefinir tipos o los problemas de distribución, es una señal de que el script está llegando a su límite.

2. PowerShell está construido sobre .NET ── los métodos [Tipo]:: y New-Object

Los cmdlets de PowerShell son, en esencia, «una parte de la biblioteca de clases .NET envuelta de forma cómoda para el trabajo operativo». Las funciones que no están envueltas también se pueden invocar directamente escribiendo el nombre del tipo entre corchetes. Los métodos estáticos se llaman con ::, y los métodos o propiedades de instancia con ..

# Invocar directamente un método estático de .NET: no requiere cmdlet ni instalación adicional
[System.IO.Path]::GetFileNameWithoutExtension('C:\data\受注_20260718.csv')  # -> 受注_20260718
[System.IO.Path]::Combine('C:\data', 'archive', '2026-07')                   # -> C:\data\archive\2026-07
[System.Math]::Round(123.456, 1)                                             # -> 123.5

# Si se necesita una instancia, crearla con New-Object o [Tipo]::new()
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('server01')

# Si se escribe ::new sin paréntesis, devuelve la lista de constructores (útil para consultar los argumentos)
[System.IO.StreamWriter]::new

[Tipo]::new() es una sintaxis añadida en PowerShell 5.0 que, además de ser más rápida que New-Object, permite consultar en el momento la lista de sobrecargas del constructor, como en el ejemplo anterior. 2 Por otro lado, hay un punto a tener en cuenta: los objetos que devuelven cmdlets como Get-Item a veces tienen propiedades adicionales (NoteProperty) añadidas por PowerShell, por lo que su composición de miembros puede no coincidir con la de un objeto del mismo tipo creado directamente con ::new(). 2 Si alguna vez se confunde porque «una propiedad que existía al obtenerlo mediante el cmdlet ha desaparecido», recuerde este mecanismo.

Una receta práctica. Para trabajar con archivos ZIP existen Compress-Archive / Expand-Archive, pero la API ZipArchive que les sirve de base tiene un límite de tamaño de archivo de 2 GB. Esta limitación está documentada oficialmente tanto para la compresión como para la descompresión, y en ambos casos se especifica que es «un límite impuesto por la API subyacente System.IO.Compression.ZipArchive», remitiendo a la referencia de esa clase. 910 Además, operaciones puntuales como «quiero ver solo la lista del contenido sin descomprimir» no existen como cmdlet. Ahí es donde entra la clase ZipFile de .NET. 11

# En Windows PowerShell 5.1 es necesario cargar explícitamente el ensamblado de la GAC
# (en PowerShell 7 el ensamblado incluido se carga automáticamente cuando se necesita, así que esta línea no es imprescindible)
Add-Type -AssemblyName System.IO.Compression.FileSystem

# Primero, la lectura ── comprobar el contenido del ZIP sin descomprimirlo
$archive = [System.IO.Compression.ZipFile]::OpenRead('C:\deploy\release.zip')
try {
    $archive.Entries | Select-Object FullName, Length, LastWriteTime
}
finally {
    $archive.Dispose()  # Garantiza que se libere el identificador de archivo
}

# Si el contenido no tiene problemas, se descomprime. Si en el destino ya existe un archivo
# con el mismo nombre, esta versión de dos argumentos lanza una excepción tras descomprimir
# parcialmente (no sobrescribe). Evite descomprimir directamente sobre una carpeta existente;
# es más seguro descomprimir siempre en una carpeta nueva y luego hacer el cambio
$dest = "C:\apps\web_$(Get-Date -Format yyyyMMddHHmmss)"
[System.IO.Compression.ZipFile]::ExtractToDirectory('C:\deploy\release.zip', $dest)

En .NET Framework, para usar la clase ZipFile se necesita una referencia al ensamblado System.IO.Compression.FileSystem, y lo mismo ocurre en PowerShell 5.1. 11 En 7 funciona sin Add-Type porque el ensamblado incluido se carga automáticamente cuando se solicita, pero si lo escribe igualmente funciona en ambos casos, así que en scripts compartidos es más prudente indicarlo explícitamente. 3

3. Incorporar C# propio y la API Win32 (P/Invoke) con Add-Type

Después de «las funciones que están en .NET» vienen «las que no están en .NET». Si pasa código fuente de C# a Add-Type, se compila en el momento y el tipo se añade a la sesión. Además, si pasa a -MemberDefinition una firma con DllImport, puede invocar la API Win32 mediante P/Invoke. Este uso es un método legítimo que también aparece como ejemplo en la documentación oficial. 3

# Avisar mediante un cuadro de diálogo cuando termine un proceso largo lanzado localmente (invoca MessageBoxW de user32.dll con P/Invoke)
$signature = @'
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
'@

$native = Add-Type -MemberDefinition $signature -Name 'NativeMethods' `
    -Namespace 'Win32' -PassThru

# Mostrar con el icono MB_ICONINFORMATION (0x40)
[void]$native::MessageBoxW([IntPtr]::Zero, 'El proceso de copia de seguridad ha finalizado.', 'Proceso de larga duración', 0x40)

Hay una advertencia. Un cuadro de diálogo modal es exclusivo para ejecuciones interactivas en las que usted mismo está frente al escritorio. En sesiones no interactivas, como una ejecución desatendida del Programador de tareas o un servicio de Windows, el proceso se quedará detenido indefinidamente en un diálogo que nadie puede cerrar. Para las notificaciones de trabajos desatendidos, use medios que no esperen una respuesta, como un archivo de registro, el registro de eventos o el envío de correo.

Si reescribe ese mismo «avisar de que el proceso ha terminado» para un trabajo desatendido, queda así: en lugar del diálogo, se escribe una entrada en el registro de eventos y, además, se deja constancia también en un archivo de registro.

# Notificación para trabajos desatendidos: no espera respuesta y deja constancia en un lugar rastreable después
$source  = 'KomuraSoft.NightlyJob'   # Nombre de origen del evento (cadena arbitraria)
$logPath = 'C:\logs\nightly.log'

# Registrar el origen requiere permisos de administrador. Ejecútelo una sola vez durante la
# implementación, y en el trabajo nocturno en sí limítese a escribir asumiendo que ya está registrado
#   New-EventLog -LogName Application -Source 'KomuraSoft.NightlyJob'

Write-EventLog -LogName Application -Source $source -EntryType Information `
    -EventId 1000 -Message 'El proceso de copia de seguridad ha finalizado.'
Add-Content -LiteralPath $logPath -Value "$(Get-Date -Format o) El proceso de copia de seguridad ha finalizado."

En el Visor de eventos, dentro de Registros de Windows > Application, si filtra por el nombre de origen KomuraSoft.NightlyJob puede seguir el historial. Si su entorno cuenta con una herramienta de monitorización, puede hacer que esta capture ese identificador de evento y lo derive a una notificación. Cabe señalar que New-EventLog / Write-EventLog son cmdlets de Windows PowerShell 5.1 y han sido eliminados en PowerShell 7 (véase la tabla de la sección 6). Si el trabajo nocturno se ejecuta en PowerShell 7, lo más directo es delegar solo la parte de notificación a powershell.exe, o bien apoyarse en el registro en el archivo de log junto con una notificación desde el lado de la vigilancia de archivos (por ejemplo, un desencadenador de eventos del Programador de tareas o Power Automate).

Add-Type tiene tres restricciones operativas importantes. 3

  • El tipo agregado solo existe dentro de esa sesión. En otra sesión o en un equipo remoto es necesario volver a ejecutar Add-Type.
  • No se puede volver a definir un tipo con el mismo nombre. Si se equivocó en la firma y quiere corregirla, cambie el nombre o inicie una sesión nueva. Realice las pruebas y errores en una consola desechable.
  • En PowerShell 7, si ya existe un tipo con el mismo nombre, se omite la propia compilación. Sospeche de esto cuando «el código que debería haber corregido no se refleja».

Una advertencia más, común a todo uso de P/Invoke. Si se equivoca en la firma (el tipo de los argumentos, el conjunto de caracteres, la convención de llamada), el fallo puede no quedarse en una excepción y hacer caer el proceso entero. Las trampas del marshaling de cadenas y handles están detalladas en «Cómo invocar la API Win32 de forma segura desde C# ── guía práctica de P/Invoke (DllImport / LibraryImport / CsWin32)», y se recomienda leerlo antes de usar la API Win32 en serio con Add-Type.

4. Invocar COM ── New-Object -ComObject y la caja de herramientas de WSH

COM es el conjunto de componentes que Windows incorpora desde antes que .NET. Se crea con New-Object -ComObject <ProgId>, y el Set objShell = CreateObject("Shell.Application") de VBScript corresponde directamente a $objShell = New-Object -ComObject Shell.Application. 4 También se pueden usar del mismo modo objetos procedentes de WSH (Windows Script Host) como WScript.Shell, WScript.Network o Scripting.FileSystemObject. 5 Para los fundamentos de qué es COM, consulte «¿Qué son COM / ActiveX / OCX? - Diferencias y relaciones explicadas».

Una receta práctica representativa es la creación masiva de accesos directos. No existe un cmdlet para crear accesos directos (.lnk), y la propia documentación oficial afirma que «para algunas tareas, como crear accesos directos, resulta más fácil usar las clases de WSH», y presenta un ejemplo con WScript.Shell. 5

# Supuesto: distribuir accesos directos a una herramienta en una carpeta compartida al escritorio público de todos los equipos
$shortcuts = Import-Csv 'C:\deploy\shortcuts.csv'   # Columnas: Name, Target, Args
$wsh = New-Object -ComObject WScript.Shell

foreach ($item in $shortcuts) {
    $lnkPath = Join-Path 'C:\Users\Public\Desktop' "$($item.Name).lnk"
    $lnk = $wsh.CreateShortcut($lnkPath)   # Si ya existe un .lnk con ese nombre, se sobrescribirá
    $lnk.TargetPath = $item.Target
    $lnk.Arguments  = $item.Args
    $lnk.Save()
    Write-Host "Creado: $lnkPath"
}

Los miembros de un objeto COM se pueden consultar con $wsh | Get-Member. 5 El abanico de aplicaciones es enorme: crear borradores de correo en Outlook, manipular carpetas especiales con Shell.Application, etc. Sin embargo, los ejecutables ActiveX como Excel.Application (servidores COM que se inician en un proceso aparte) arrastran el problema de limpieza que se trata en el capítulo siguiente. La propia documentación oficial advierte que si el proceso termina o no al soltar la referencia depende de la aplicación en cuestión, y que conviene probar ese comportamiento de cierre antes de usarla. 5

5. La limpieza de COM ── el problema de que EXCEL.EXE queda residual y «Office desatendido no es compatible»

5.1. Por qué queda el proceso residual

El ciclo de vida de un objeto COM se gestiona por conteo de referencias. Cuando toca COM desde .NET (es decir, desde PowerShell), se crea un proxy llamado RCW (Runtime Callable Wrapper) para cada objeto COM, y mientras ese RCW siga vivo, la referencia del lado COM no se libera. La recolección del RCW queda en manos del recolector de basura. 6 Es decir, aunque llame a $excel.Quit(), si en algún sitio queda una referencia viva (incluido el RCW de un objeto intermedio que no asignó a ninguna variable, como en $excel.Workbooks.Open(...)), EXCEL.EXE no terminará.

El siguiente código tiene un try/finally anidado en tres niveles, y a primera vista la estructura es difícil de leer. Antes de nada, expliquemos por qué se escribe así. En resumen, cuanto más tardío es un paso, menos se quiere que se salte, así que simplemente se colocan en el interior en el orden en que menos se quiere que se salten.

  • El try más externo: el procesamiento principal que manipula Excel. Sea lo que sea lo que falle aquí, siempre se entra en la limpieza posterior.
  • finally de primer nivel (cerrar el libro): aunque falle el guardado, se intenta cerrar el libro que se abrió. Sin embargo, el propio Close también puede fallar.
  • finally de segundo nivel (llamar a Quit): por eso Quit se llama en una posición que no se ve arrastrada por el fallo de Close. Si Excel deja de responder, Quit también puede fallar.
  • finally de tercer nivel (liberar el RCW y ejecutar el GC): por eso, sin importar si Quit tuvo éxito o no, la liberación de referencias se ejecuta siempre. Si este paso se salta, el proceso queda residual.

En otras palabras, la triple anidación no es un adorno estilístico, sino el resultado de eliminar, un nivel a la vez, la premisa de que la propia limpieza puede fallar. Dicho de otro modo, si puede asumir que ni Close ni Quit fallarán, con un único finally basta.

$excel = New-Object -ComObject Excel.Application
try {
    $excel.DisplayAlerts = $false
    $books = $excel.Workbooks              # Recibir también el objeto intermedio en una variable para poder liberarlo después
    $book  = $books.Open('C:\work\月次売上.xlsx')
    $sheet = $book.Worksheets.Item(1)
    $sheet.Cells.Item(1, 1).Value2 = "Actualizado: $(Get-Date -Format 'yyyy-MM-dd')"
    $book.Save()
}
finally {
    # Precisamente la ruta en la que Close falla es la que suele dejar EXCEL.EXE residual,
    # por eso se ejecutan siempre Quit y la liberación en un finally anidado
    try {
        if ($book) { $book.Close($false) }
    }
    finally {
        # Aunque Quit falle (Excel sin respuesta, servidor COM desconectado, etc.), la liberación del RCW se realiza siempre
        try {
            if ($excel) { $excel.Quit() }
        }
        finally {
            # Reducir explícitamente el conteo de referencias del RCW (del hijo al padre)
            foreach ($obj in @($sheet, $book, $books, $excel)) {
                if ($obj) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($obj) }
            }
            # Salvaguarda para que el GC recolecte los RCW que no se recibieron en variable
            [System.GC]::Collect()
            [System.GC]::WaitForPendingFinalizers()
            [System.GC]::Collect()
        }
    }
}

Marshal.ReleaseComObject reduce el conteo de referencias del RCW y, cuando llega a 0, libera la referencia del lado COM. Sin embargo, la documentación oficial advierte con firmeza que acceder a un RCW ya liberado provoca excepciones o violaciones de acceso, por lo que debe «usarse únicamente cuando sea absolutamente necesario». 7

El hecho de que en el código anterior se reciba cada objeto intermedio en su propia variable es una práctica que se conoce popularmente como «la regla de los dos puntos». Si encadena dos o más puntos, como en $excel.Workbooks.Open(...), el RCW correspondiente al Workbooks intermedio se genera sin quedar asignado a ninguna variable, y no queda forma de liberarlo. Por eso la regla consiste en usar como máximo un punto por línea y recibir siempre en una variable el objeto intermedio. Los estilos de liberación (la corriente de ReleaseComObject frente a la del GC), los detalles de la regla de los dos puntos y cómo identificar un proceso que aun así queda residual se explican en detalle en «El problema de EXCEL.EXE residual al operar Excel desde C# ── patrones de liberación de referencias COM y el criterio de sustitución». Es un artículo sobre C#, pero el mecanismo del RCW es idéntico en PowerShell.

5.2. Ante todo, no usar Office en ejecuciones desatendidas

Hay una decisión más de fondo. Microsoft declara oficialmente que «actualmente no recomienda ni admite la automatización de Office desde aplicaciones cliente o componentes desatendidos y no interactivos (incluidos ASP, ASP.NET, DCOM y servicios de NT)». Esto se debe a que Office está diseñado bajo la premisa de un uso interactivo, y en entornos desatendidos puede mostrar comportamientos inestables o interbloqueos. 8 También se enumeran problemas concretos, como que el proceso se detenga ante un diálogo inesperado o que, al basarse en STA, no resista la ejecución múltiple en paralelo. 8 Aquí, STA es la abreviatura de apartamento de un solo subproceso (Single-Threaded Apartment), uno de los modelos de subprocesamiento de COM. Es un mecanismo que concentra las llamadas a un objeto en el subproceso que lo creó y las procesa una a una, y las aplicaciones de Office están construidas sobre esta premisa. Por eso, estructuralmente, no se prestan al uso de «ejecutar en paralelo muchas instancias del mismo proceso en un solo servidor». El modelo de subprocesamiento de COM en sí se trata en detalle en «Fundamentos de COM STA/MTA - el modelo de subprocesamiento y cómo evitar bloqueos».

En resumen, una configuración que invoca Excel por COM desde un trabajo nocturno del Programador de tareas o desde un servicio no es compatible desde el momento mismo en que «parece funcionar». Para los procesos desatendidos se recomienda sustituirlo por la edición directa de archivos de la familia Open XML o por la integración con CSV, sin iniciar la aplicación de Office en sí. 8 Las recetas concretas de sustitución en PowerShell se recopilan en «Recetas de procesamiento de Excel y CSV con PowerShell», publicado simultáneamente. Trace la línea de modo que el uso de Excel por COM se limite a «asistir el trabajo de una persona en un escritorio donde esa persona ha iniciado sesión».

6. El «.NET disponible» difiere entre 5.1 y 7

Windows PowerShell 5.1 está construido sobre la serie .NET Framework 4.5, y PowerShell 7 sobre .NET (antes .NET Core). 1 Mientras solo se usen cmdlets, las ocasiones en que hay que tener presente esta diferencia son limitadas, pero en cuanto se empieza a invocar .NET directamente, como en este artículo, la diferencia sale a la superficie.

Punto Windows PowerShell 5.1 PowerShell 7
Base Serie .NET Framework 4.51 .NET (se actualiza en cada versión; por ejemplo, 7.4 usa .NET 8.0)1
Sobrecargas de métodos Pocas (ejemplo: String.Split tiene 6 variantes)1 Muchas (la documentación oficial presenta casos donde el mismo Split('pq') produce resultados distintos)1
Carga de ensamblados Los ensamblados de la GAC a menudo requieren carga explícita con Add-Type3 Los ensamblados incluidos se cargan automáticamente cuando se solicitan3
Qué dejó de estar disponible Se eliminaron algunos cmdlets exclusivos de Windows, como *-EventLog1

Pueden darse tanto casos de «funcionaba en 5.1 pero no en 7» como el caso contrario. Si un script debe funcionar en ambos, pruébelo siempre en los dos entornos. El panorama completo del criterio de migración se trata en «Diferencias y migración entre Windows PowerShell 5.1 y PowerShell 7», publicado simultáneamente.

7. Reglas prácticas habituales (tabla de decisión)

Punto Opciones (la recomendada se marca como «recomendado») Criterio de decisión
Lo que quiere hacer no existe como cmdlet Renunciar / invocar la clase .NET directamente (recomendado) Busque primero [Tipo]::Método. La mayoría de los «solo me falta esto» se cubren en System.IO, System.Text o System.IO.Compression1
Creación de instancias New-Object / [Tipo]::new() (recomendado) Si solo debe funcionar desde 5.0, use ::new(): muestra la lista de constructores y es más rápido. Para COM, la única opción es New-Object -ComObject24
Se necesita la API Win32 Resignarse a hacerlo a mano / P/Invoke con Add-Type (recomendado) Para unas pocas API, Add-Type es suficiente. Un error de firma hace caer el proceso entero, así que valide en una sesión desechable3
Operaciones de origen WSH, como crear accesos directos COM (WScript.Shell) (recomendado) En los ámbitos sin cmdlet, COM sigue vigente hoy. El propio ejemplo oficial usa este método5
Operación de Excel que asiste el trabajo de una persona COM + limpieza rigurosa (recomendado) En el finally, incluya siempre Quit, ReleaseComObject y GC como conjunto. Reciba también los objetos intermedios en variables76
Procesamiento de Excel/Office en trabajos desatendidos COM / método que no inicie Office (recomendado) La automatización desatendida de Office no es compatible. Sustitúyala por la familia Open XML o por CSV8
El script ha crecido demasiado Insistir con PowerShell / convertirlo en herramienta C# (recomendado) Es señal de migración si el código de Add-Type llega a cientos de líneas, la restricción de no poder redefinir tipos entorpece el desarrollo, o no quiere que se recompile cada vez en el destino de distribución

La última fila es la decisión con la que cierra este artículo. Cuando el C# incrustado con Add-Type empieza a crecer desmesuradamente, ya se encuentra en el estado de «un programa C# envuelto en la piel de PowerShell». En ese punto, tanto el desarrollo como el mantenimiento avanzan más rápido si lo traslada a un proyecto C# donde pueda aprovechar la verificación de tipos y el depurador de Visual Studio, los paquetes NuGet y las pruebas unitarias. También existen formas de volver a invocar los recursos de PowerShell desde el lado de C#, así que la migración no implica reescribirlo todo. En «Cómo ejecutar PowerShell desde C# (CSharp) y recibir el resultado como objetos» se presentan los patrones para tender ese puente.

8. Resumen

  • PowerShell está construido sobre .NET, y puede invocar directamente la biblioteca de clases .NET con [Tipo]::método estático, New-Object o [Tipo]::new(). Para las funciones que no tiene un cmdlet, .NET es la primera opción.
  • Con Add-Type puede compilar código C# en el momento, y si le pasa una firma con DllImport también puede invocar la API Win32 mediante P/Invoke. El tipo solo existe dentro de la sesión y no se puede redefinir con el mismo nombre.
  • COM se maneja con New-Object -ComObject. Las herramientas de origen WSH, como la creación de accesos directos, siguen vigentes hoy.
  • El ciclo de vida de COM se gestiona mediante conteo de referencias más el RCW, y si queda alguna referencia, procesos como EXCEL.EXE quedan residuales. Escriba en el finally una limpieza que incluya Quit, ReleaseComObject y el GC.
  • Microsoft declara explícitamente que la automatización de Office en ejecuciones desatendidas no está recomendada ni es compatible. Sustituya los trabajos nocturnos por un método que no inicie Office.
  • La base .NET difiere entre 5.1 y 7, y también los tipos y sobrecargas disponibles. Los scripts que deben funcionar en ambos requieren pruebas obligatorias en los dos entornos.
  • Si Add-Type crece demasiado, es señal de convertirlo en herramienta C#. Ya existen patrones establecidos para tender el puente entre el script y el ejecutable.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa de la automatización de operaciones internas con PowerShell, del diseño y la modificación de scripts que incluyen activos COM (integración con Excel, componentes heredados), y de convertir a herramientas en C# los scripts que «han crecido demasiado». También atendemos investigaciones de fallos como el de EXCEL.EXE residual.

Referencias

  1. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sobre que Windows PowerShell 5.1 está construido sobre .NET Framework 4.5 y PowerShell 6.0 en adelante sobre .NET Core (actual .NET), la lista de versiones de .NET que sirven de base a cada versión, un ejemplo en el que el mismo código produce resultados distintos por la diferencia de sobrecargas de String.Split, y los cmdlets eliminados en 7.  2 3 4 5 6 7 8 9

  2. Microsoft Learn, about_Object_Creation. Sobre el método estático new() añadido a todos los tipos .NET en PowerShell 5.0, que al escribir ::new se puede consultar la lista de sobrecargas del constructor, y que los objetos obtenidos mediante un cmdlet pueden no coincidir en sus miembros con los creados mediante ::new() porque PowerShell les añade NoteProperty.  2 3 4

  3. Microsoft Learn, Add-Type. Sobre que Add-Type compila código fuente de C# y añade el tipo a la sesión, el ejemplo oficial de P/Invoke mediante MemberDefinition (ShowWindowAsync de user32.dll), que el tipo agregado solo existe dentro de la sesión y no se puede redefinir con el mismo nombre, que en PowerShell 7 se omite la compilación si ya existe un tipo con el mismo nombre, y que en 5.1 se necesita Add-Type para cargar ensamblados de la GAC mientras que desde la versión 6 se cargan automáticamente.  2 3 4 5 6 7 8

  4. Microsoft Learn, New-Object. Sobre que New-Object crea una instancia de un objeto .NET o COM, que el parámetro -ComObject recibe un ProgId, y que el CreateObject(“Shell.Application”) de VBScript corresponde a New-Object -ComObject “Shell.Application”.  2 3

  5. Microsoft Learn, Creating .NET and COM objects (New-Object). Sobre la creación de objetos WSH como WScript.Shell, la afirmación de que crear accesos directos es más fácil con las clases de WSH junto con un ejemplo real de CreateShortcut, la aplicación de Get-Member a objetos COM, que el comportamiento de cierre de un ejecutable ActiveX depende de la aplicación y requiere pruebas previas, y que New-Object usa el RCW de .NET.  2 3 4 5 6

  6. Microsoft Learn, Runtime Callable Wrapper. Sobre que .NET expone los objetos COM mediante un proxy llamado RCW, que se crea un RCW por cada objeto COM, y que el RCW libera la referencia al objeto COM cuando el recolector de basura lo recoge.  2 3

  7. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. Sobre que ReleaseComObject reduce el conteo de referencias del RCW y libera el objeto COM subyacente al llegar a 0, que usar un RCW ya liberado provoca excepciones o violaciones de acceso, la advertencia oficial de «usarlo únicamente cuando sea absolutamente necesario», y su relación con FinalReleaseComObject.  2 3

  8. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Sobre que Microsoft no recomienda ni admite la automatización de Office desde aplicaciones cliente o componentes desatendidos y no interactivos (incluidos ASP, ASP.NET, DCOM y servicios de NT), la posibilidad de comportamientos inestables o interbloqueos en entornos desatendidos, problemas concretos como la detención por diálogos o las restricciones de ejecución múltiple derivadas de STA, y que la edición directa del formato de archivo Open XML es la alternativa recomendada.  2 3 4 5

  9. Microsoft Learn, Expand-Archive. Sobre que Expand-Archive usa la API System.IO.Compression.ZipArchive, que esta API tiene un límite de tamaño máximo de archivo de 2 GB, y que esta API de .NET maneja archivos conformes a la especificación oficial del formato ZIP de PKWARE. 

  10. Microsoft Learn, Compress-Archive. Sobre que Compress-Archive comprime usando la API System.IO.Compression.ZipArchive, que se indica explícitamente «The API limits the maximum file size to 2GB» como una limitación del lado de la API subyacente, y que como referencia se menciona la ZipArchive Class (dado que el texto de referencia de la clase ZipArchive no menciona la cifra de 2 GB, la fuente primaria de ese número es esta página del cmdlet). 

  11. Microsoft Learn, ZipFile Class. Sobre que la clase ZipFile ofrece métodos estáticos para crear, extraer y abrir archivos ZIP, que en .NET Framework se necesita una referencia al ensamblado System.IO.Compression.FileSystem, y sobre el uso de CreateFromDirectory, ExtractToDirectory y OpenRead.  2

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Se pueden invocar clases .NET directamente desde PowerShell?
Sí. Como PowerShell está construido sobre .NET, puede escribir el nombre del tipo entre corchetes, como en [System.IO.Path]::GetFileNameWithoutExtension(), y llamar a métodos estáticos con ::. Si necesita una instancia, puede crearla con New-Object o, desde PowerShell 5.0, con [Tipo]::new(). Incluso las operaciones para las que no existe un cmdlet se pueden usar desde el script sin instalación adicional, siempre que estén disponibles en la biblioteca de clases .NET.
¿Debo usar New-Object o [Tipo]::new()?
Ambos permiten crear objetos, pero [Tipo]::new() escrito sin argumentos muestra la lista de constructores del tipo, lo que resulta útil para consultar los argumentos disponibles mientras escribe el código. Además, [Tipo]::new() ofrece mejor rendimiento. Por otro lado, la creación de objetos COM solo es posible con New-Object -ComObject. Si también debe contemplar entornos anteriores a PowerShell 5.0, use New-Object.
¿Por qué queda EXCEL.EXE residual al operar Excel por COM desde PowerShell?
El ciclo de vida de un objeto COM se gestiona mediante conteo de referencias, y del lado de .NET es el RCW (Runtime Callable Wrapper) el que retiene esa referencia. Si escribe algo como $excel.Workbooks.Open(), también se crea un RCW invisible para el objeto Workbooks intermedio, y queda una referencia que no está asignada a ninguna variable. Aunque llame a Quit(), el proceso no terminará si todavía queda alguna referencia viva. Al terminar de usarlo es necesario liberar explícitamente con Marshal.ReleaseComObject, o bien poner las variables en null y dejar que GC.Collect() junto con WaitForPendingFinalizers() se encarguen de recolectarlas.
¿Es correcto operar Excel por COM desde un servidor o un proceso batch nocturno?
No se recomienda. Microsoft declara oficialmente que no recomienda ni admite la automatización de aplicaciones de Office desde aplicaciones cliente o componentes desatendidos y no interactivos. Office está diseñado bajo la premisa de un uso interactivo en escritorio, por lo que en ejecuciones desatendidas puede presentar comportamientos inestables o interbloqueos (deadlocks). En procesos desatendidos, lo habitual es sustituirlo por métodos que no inicien la aplicación de Office en sí, como las bibliotecas de la familia Open XML o la integración con CSV.
¿Se puede invocar la API Win32 con Add-Type?
Sí. Si pasa a -MemberDefinition de Add-Type una firma de C# con DllImport, se compila en el momento y queda disponible para invocar la API Win32 mediante P/Invoke. La documentación oficial incluye incluso un ejemplo que llama a ShowWindowAsync de user32.dll. Sin embargo, el tipo agregado permanece dentro de la sesión y no se puede volver a definir un tipo con el mismo nombre, así que durante la fase de prueba y error conviene reiniciar en una sesión nueva. Un error en la firma puede llegar a hacer caer el proceso entero, por lo que es más seguro verificar el funcionamiento en una consola desechable.
¿Hay diferencias al invocar .NET entre Windows PowerShell 5.1 y PowerShell 7?
Sí las hay. 5.1 está construido sobre la serie .NET Framework 4.5, mientras que PowerShell 7 lo está sobre .NET (antes .NET Core), por lo que difieren los tipos disponibles y las sobrecargas de métodos. Por ejemplo, String.Split tiene más sobrecargas en 7, y la documentación oficial presenta casos en los que el mismo código produce resultados distintos. Además, en 5.1 hay muchas situaciones en las que hace falta cargar mediante Add-Type los ensamblados de la GAC, mientras que en 7 los ensamblados incluidos se cargan automáticamente cuando se necesitan. Si su script debe funcionar en ambos, pruébelo en los dos entornos.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog