Warum EXCEL.EXE-Prozesse nach C#-Excel-COM-Automatisierung bestehen bleiben — Muster zur Referenzfreigabe und die Ersatzentscheidung

· · Excel, C#, COM, .NET, .NET Framework, Office, Pflege von Altsystemen, Technische Beratung

„Wir haben eine Funktion gebaut, die Berichte nach Excel ausgibt, und im Task-Manager stand am Ende eine lange Reihe von EXCEL.EXE-Einträgen.“ „Ich habe die App geschlossen, aber beim nächsten Öffnen der Datei heißt es: ‚Diese Datei wird bereits von einem anderen Prozess verwendet.‘“ „Auf dem Nachtstapel-Server sammelten sich Hunderte Excel-Prozesse an und fraßen den gesamten Speicher, bis alles zum Stillstand kam.“ Fast jeder Entwickler, der C#-Code geschrieben hat, der Excel über Microsoft.Office.Interop.Excel steuert, stößt mindestens einmal darauf. Wir werden regelmäßig mit „ich rufe Quit() auf, aber Excel will nicht beenden“ konsultiert.

Was die Sache lästig macht, ist, dass es aussieht, als würde es „nur manchmal“ passieren. Der Prozess verschwindet auf einer Entwicklungsmaschine, bleibt aber in der Produktion bestehen; er bleibt unter dem Debugger bestehen, verschwindet aber im Release-Build — da sich die Reproduktionsbedingungen ständig verschieben, schleicht sich ein symptomatisches Process.Kill gerne als Behelf in den Produktionscode. Doch die Ursache ist keineswegs Zufall — sie lässt sich vollständig durch COMs Referenzzählung und die Mechanik von .NETs RCW (Runtime Callable Wrapper) erklären. Dieser Artikel arbeitet aus praktischer Sicht durch: den Mechanismus hinter dem verbleibenden Prozess, die klassische Falle der „Zwei-Punkt-Regel“, einen Vergleich der beiden Schulen von Freigabemustern samt unserer Empfehlung, die korrekte Art, als letztes Mittel einen Prozess zu beenden, sowie die Frage, wann COM-Interop durch eine Open-XML-basierte Bibliothek ersetzt werden sollte.

1. Die Kurzfassung

  • Dass EXCEL.EXE am Leben bleibt, ist kein Bug — es liegt daran, dass die .NET-Seite noch eine COM-Referenz hält. Quit() ist nichts weiter als eine Bitte, die besagt „beende dich, sobald jede Referenz freigegeben wurde“; solange eine Referenz übrig bleibt, wartet Excel pflichtbewusst weiter.
  • .NET bedient COM-Objekte über einen als RCW (Runtime Callable Wrapper) bezeichneten Wrapper, und er hält eine Referenz auf das COM-Objekt so lange, bis der RCW vom GC eingesammelt (oder explizit freigegeben) wird.1
  • Die Verkettung von zwei oder mehr Punkten, wie in book.Worksheets[1].Range["A1"], erzeugt RCWs für die Zwischenobjekte, die nie einer Variable zugewiesen werden, und sie werden am Ende nie freigegeben. Dies ist gemeinhin als „Zwei-Punkt-Regel“ bekannt und der Hauptverdächtige hinter diesem Problem.
  • Es gibt zwei Denkschulen zur Freigabe von Referenzen: (a) diszipliniertes Anwenden von Marshal.ReleaseComObject auf jedes COM-Objekt und (b) das Beschränken von Referenzen auf lokale Variablen und das Einsammeln durch den GC (GC.Collect + WaitForPendingFinalizers). Die offizielle Dokumentation positioniert ReleaseComObject als etwas, das „nur verwendet werden sollte, wenn es unbedingt erforderlich ist“.2
  • Unsere Empfehlung lautet, standardmäßig auf das GC-Muster mit in einer einzigen Methode isolierter Arbeit zu setzen, und nur dann einen kleinen using-basierten Wrapper, der die Freigabe diszipliniert, einzuführen, wenn Sie die Freigabereihenfolge tatsächlich kontrollieren müssen (Abschnitt 4).
  • Als Absicherung für Fälle, in denen der Prozess weiterhin nicht verschwindet, erfassen Sie das Fenster-Handle aus Application.Hwnd, lösen Sie dessen PID mit GetWindowThreadProcessId auf und beenden Sie diesen Prozess. Verwenden Sie nicht die Methode „Prozessliste vor und nach dem Start vergleichen“, um das Ziel zu identifizieren — sie riskiert, eine vom Benutzer geöffnete Excel-Instanz zu beenden.34
  • Und noch grundsätzlicher: Microsoft unterstützt keine Office-Automatisierung in serverseitigen, unbeaufsichtigten Umgebungen.5 Wenn Sie Berichte unbeaufsichtigt erzeugen, erwägen Sie zunächst, COM-Interop durch Open XML SDK / ClosedXML zu ersetzen, das Excel überhaupt nicht startet (Abschnitte 6–7).67

2. Warum EXCEL.EXE weiterläuft — Referenzzählung und der RCW

Beginnen wir mit dem Prinzip auf der COM-Seite. Die Lebensdauer eines COM-Objekts wird über einen Referenzzähler verwaltet, und der Automatisierungsserver von Excel (EXCEL.EXE) beendet sich erst, wenn jede an externe Clients ausgegebene Referenz zurückgegeben wurde. Schon in der VB6-Ära hinterließ ein vergessener Aufruf von Release den Prozess auf genau dieselbe Weise (eine Auffrischung zu COM finden Sie in „Was sind COM / ActiveX / OCX?“).

Nun zur .NET-Seite. C#-Code fasst ein COM-Objekt nie direkt an — er bedient es über einen vom CLR erzeugten Proxy namens RCW. Pro COM-Objekt wird innerhalb eines Prozesses genau ein RCW erzeugt; er cacht den COM-Schnittstellenzeiger und gibt seine Referenz auf das COM-Objekt frei, wenn er selbst vom GC eingesammelt wird.1 Die Lebensdauerverwaltung ist also vom „selbst den Referenzzähler zählen“ hin zum „dem GC überlassen“ gewechselt.

Kombiniert man diese beiden Tatsachen, ergibt sich der Grund, warum EXCEL.EXE weiterläuft, ganz von selbst.

Phase Was passiert
new Excel.Application() EXCEL.EXE startet, und der RCW für das Application-Objekt wird erzeugt
Zellenoperationen, Speichern usw. Für jedes angefasste Objekt sammelt sich ein RCW an — Workbook, Worksheet, Range und so weiter
excel.Quit() Sagt Excel nur, „du darfst dich jetzt beenden“. Nicht eine einzige von einem RCW gehaltene Referenz wird freigegeben
Rückkehr der Methode Die .NET-Referenz auf den RCW verschwindet, doch der RCW selbst lebt weiter auf dem Heap
GC (wann auch immer er läuft) Der RCW wird eingesammelt, und erst dann wird die COM-Referenz zurückgegeben — das erlaubt EXCEL.EXE erst, sich zu beenden

Zwei Dinge sind mitzunehmen. Erstens: Quit() ist keine Freigabe. Solange eine Referenz übrig bleibt, wartet Excel. Unter normalen Umständen, sobald der Host-Anwendungsprozess vollständig beendet ist, verschwindet die Referenz und Excel beendet sich ebenfalls — doch in Formen, in denen der Elternprozess weiterlebt, etwa eine residente App oder eine Web-Anwendung, kommt dieses „irgendwann“ nie. Zweitens: der Zeitpunkt der Freigabe ist unbestimmt, weil er vom GC abhängt. Ist reichlich Speicher vorhanden, läuft der GC möglicherweise zig Minuten nicht, und während dieser ganzen Zeit steht EXCEL.EXE als Zombie herum. Die fehlende Reproduzierbarkeit — „manchmal bleibt es bestehen“, „nur in der Produktion bleibt es bestehen“ — ist nichts weiter als die Zeitplanung des GC, die sich abzeichnet.

Beachten Sie, dass ein mit Visible = false gestartetes Excel kein Fenster besitzt, sodass eine verbleibende Instanz für den Benutzer unsichtbar ist. Das Symptom zeigt sich typischerweise als „Fehler ‚Datei wird verwendet‘ beim zweiten Speichern“ oder „der PC fühlt sich langsam an“, und erst wenn jemand den Task-Manager öffnet, bemerkt man die Reihe von EXCEL.EXE-Einträgen. Der erste Schritt jeder Untersuchung ist, sie mit tasklist | findstr EXCEL zu zählen.

3. Die klassische Falle: die „Zwei-Punkt-Regel“ — unsichtbare Zwischenobjekte

Code hinter der Beschwerde „ich gebe jede Variable ordentlich frei, und es bleibt trotzdem bestehen“ enthält fast immer eine Zeile wie diese.

// Wirkt auf den ersten Blick sauber, erzeugt aber RCWs, die nie freigegeben werden können
excel.Workbooks.Open(path);
book.Worksheets[1].Range["A1"].Value2 = "hello";

excel.Workbooks erzeugt und liefert einen RCW für die Workbooks-Sammlung zurück. Rufen Sie darauf .Open(...) auf, ohne den Rückgabewert in einer Variable zu erfassen, bleibt der Workbooks-RCW auf dem Heap als anonymes Objekt zurück, auf das nichts verweist, das aber trotzdem lebt. Da es keine Variable gibt, gibt es auch keine Möglichkeit, darauf Marshal.ReleaseComObject aufzurufen. Die zweite Zeile ist noch schlimmer — sie erzeugt in einer einzigen Zeile drei anonyme RCWs: Worksheets (die Sammlung), Worksheets[1] (das Blatt) und Range["A1"] (der Bereich).

Die in der Office-Automatisierungs-Community lange bekannte Faustregel, dies zu vermeiden, ist die „Zwei-Punkt-Regel“. Anders ausgedrückt: Verketten Sie niemals zwei oder mehr Punkte an einem COM-Objekt — erfassen Sie jedes Zwischenobjekt zuerst in einer Variable.

// Jedem Zwischenobjekt einen Namen geben
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(path);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";

Das wirkt umständlich, aber der Punkt ist, jedes freizugebende Objekt aufzählbar zu halten. Hier einige leicht zu übersehende Varianten desselben Musters.

  • foreach: foreach (Excel.Worksheet s in book.Worksheets) erzeugt RCWs für die Sammlung, den Enumerator und jedes Element. In der ReleaseComObject-Schule ist es Standardpraxis, eine indizierte for-Schleife zu verwenden, die jedes Element einzeln in eine Variable erfasst.
  • Wegwerfnutzung innerhalb eines Bedingungsausdrucks: Ein RCW wird auch innerhalb eines Ausdrucks wie if (excel.Workbooks.Count > 0) erzeugt.
  • Ein zusammengesetzter Ausdruck als Argument: Ein Ausdruck wie sheets.Add(After: sheets[sheets.Count]) erzeugt in einer einzigen Zeile mehrere anonyme RCWs.
  • Ereignisabonnements: Das Anhängen eines Handlers an ein Application- oder Workbook-Ereignis hält über diese Verbindung eine Referenz am Leben. Bestellen Sie das Abonnement vor dem Herunterfahren immer ab.

4. Vergleich der Freigabemuster — die ReleaseComObject-Schule und die GC-Schule

Es gibt zwei Denkschulen für Code, der EXCEL.EXE zuverlässig beendet. Beide funktionieren, wenn sie korrekt geschrieben sind. Die eigentliche Frage ist, ob Sie sie weiterhin korrekt schreiben können, und genau dort zeigt sich der praktische Unterschied.

4.1 (a) Diszipliniertes Anwenden von Marshal.ReleaseComObject

Marshal.ReleaseComObject verringert den internen Referenzzähler des RCW, und in dem Moment, in dem er null erreicht, wird die vom RCW gehaltene COM-Referenz sofort freigegeben.2 Der Vorteil ist, dass die Freigabe zu einem deterministischen Zeitpunkt geschieht, ohne auf den GC zu warten. Hier die typische Form, wenn dies auf jedes Objekt angewendet wird.

using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;

Excel.Application excel = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range cell = null;
try
{
    // Keinen Objektinitialisierer verwenden (new ... { DisplayAlerts = false }).
    // Schlägt der COM-Aufruf des Setters fehl, wäre excel bei Erreichen von
    // finally immer noch nicht zugewiesen, sodass es keine Möglichkeit gibt,
    // das bereits gestartete EXCEL.EXE per Quit oder Freigabe zu beenden
    excel = new Excel.Application();
    excel.DisplayAlerts = false;
    books = excel.Workbooks;
    book = books.Open(templatePath);
    sheets = book.Worksheets;
    sheet = (Excel.Worksheet)sheets[1];
    cell = sheet.Range["A1"];
    cell.Value2 = "hello";
    book.SaveAs(outputPath);
}
finally
{
    // In umgekehrter Erzeugungsreihenfolge freigeben. Auch Close und Quit selbst
    // sind COM-Aufrufe, die fehlschlagen können, also verschachtelte try/finally-
    // Blöcke verwenden, um zu garantieren, dass Quit und die Freigabe auch dann
    // erreicht werden, wenn mittendrin etwas eine Ausnahme wirft
    if (cell   != null) Marshal.ReleaseComObject(cell);
    if (sheet  != null) Marshal.ReleaseComObject(sheet);
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    try
    {
        if (book != null) book.Close(SaveChanges: false);
    }
    finally
    {
        if (book  != null) Marshal.ReleaseComObject(book);
        if (books != null) Marshal.ReleaseComObject(books);
        try
        {
            if (excel != null) excel.Quit();
        }
        finally
        {
            if (excel != null) Marshal.ReleaseComObject(excel);
        }
    }
}

Die Schwäche dieses Ansatzes ist, wie Sie sehen, dass die erforderliche Disziplin teuer ist. Jedes einzelne von Ihnen angefasste COM-Objekt muss in einer Variable erfasst und in umgekehrter Reihenfolge freigegeben werden, einschließlich entlang von Ausnahmepfaden. Und berücksichtigt man, dass Close oder Quit selbst fehlschlagen kann (ein COM-Fehler, eine getrennte Arbeitsmappe, ein nicht reagierendes Excel), müssen Sie — mit verschachteltem try/finally wie oben gezeigt — garantieren, dass ein Fehlschlag mittendrin trotzdem die nachfolgenden Freigaben erreicht. Nach unserer Erfahrung ist es eine wirklich schwierige Erwartung, dass jedes Teammitglied dies bei jeder einzelnen Änderung durchhält. Ein einziger versprengter Zwei-Punkt-Ausdruck an irgendeiner Stelle bringt das Leck sofort zurück.

Noch wichtiger ist, dass die offizielle Dokumentation selbst vor Missbrauch warnt. Die Referenz zu Marshal.ReleaseComObject besagt unmissverständlich, dass es ein Werkzeug für Fälle ist, in denen Ressourcen umgehend freigegeben werden müssen oder die Freigabereihenfolge zählt, und dass Sie „ReleaseComObject nur verwenden sollten, wenn es unbedingt erforderlich ist“.2 Da ein RCW gemeinsam genutzt wird — genau einer pro COM-Objekt und Prozess —, führt es zu einer InvalidComObjectException oder im schlimmsten Fall zu einer Zugriffsverletzung oder Speicherbeschädigung des Prozesses, wenn Code an einer Stelle einen RCW freigibt, den ein anderer Teil der Codebasis noch verwendet.2 In einer Architektur, in der mehrere Module innerhalb einer App Excel-Operationen gemeinsam nutzen, ist diese Art von Unfall ein reales Risiko.

Noch ein Detail: Wird derselbe Schnittstellenzeiger wiederholt an den CLR übergeben, kann der interne Referenzzähler des RCW über 1 hinausgehen, sodass ein einzelner Aufruf ihn nicht freigibt. Es gibt auch Marshal.FinalReleaseComObject, das den Zähler zwangsweise auf 0 setzt2, doch der Moment, in dem Sie diese API brauchen, ist selbst ein Zeichen dafür, dass Sie den Überblick über die Lebensdauer des Objekts verloren haben, und wir empfehlen, stattdessen das Design zu überdenken.

Ein Sicherheitshinweis, der eine von der Prozessleckage unabhängige Achse betrifft. Eine über Workbooks.Open per COM-Automatisierung geöffnete Arbeitsmappe kann VBA ohne jede Makro-Warnung ausführen. Das Beispiel in diesem Artikel setzt voraus, dass eine vertrauenswürdige, von der eigenen App verwaltete Vorlage geöffnet wird, doch besteht auch nur die Möglichkeit, eine Arbeitsmappe aus einer externen Quelle zu öffnen — eine Datei in einer Freigabe, etwas von einem Benutzer Hochgeladenes —, setzen Sie vor dem Aufruf von Open excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable (im Namensraum Microsoft.Office.Core), um Makros zwangsweise zu deaktivieren.8 Das schließt den Angriffsweg, bei dem das Austauschen einer Vorlage direkt zur Ausführung beliebigen Codes wird. Dieser Vorbehalt gilt für das GC-Muster-Beispiel unten ebenso.

4.2 (b) Referenzen beschränken und dem GC das Einsammeln überlassen

Der andere Ansatz besteht darin, die Freigabe des RCW genau so, wie der Mechanismus entworfen wurde, dem GC zu überlassen und diesen GC zu einem deterministischen Zeitpunkt auszuführen. Da ein RCW seine COM-Referenz freigibt, wenn der GC ihn einsammelt1, können Sie EXCEL.EXE beenden, ohne einen einzigen Aufruf von ReleaseComObject zu schreiben, indem Sie „einen vollständigen GC plus das Warten auf den Abschluss von Finalizern ausführen, nachdem jede Excel berührende Referenz den Gültigkeitsbereich verlassen hat“.

using System.Runtime.CompilerServices;
using Excel = Microsoft.Office.Interop.Excel;

public void ExportReport(string templatePath, string outputPath)
{
    try
    {
        // Die Excel berührende Arbeit vollständig in eine separate Methode isolieren
        ExportReportCore(templatePath, outputPath);
    }
    finally
    {
        // Gerade auf dem Ausnahmepfad neigt EXCEL.EXE dazu, hängen zu bleiben,
        // daher immer in finally ausführen. Sobald die Methode zurückgekehrt
        // ist, bleibt nirgends mehr eine Referenz auf einen RCW bestehen
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();   // Zweiter Durchlauf, um die vom Finalizer gelösten RCWs einzusammeln
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath)
{
    var excel = new Excel.Application();
    try
    {
        excel.DisplayAlerts = false;
        Excel.Workbooks books = excel.Workbooks;
        Excel.Workbook book = books.Open(templatePath);
        Excel.Sheets sheets = book.Worksheets;
        Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
        Excel.Range cell = sheet.Range["A1"];
        cell.Value2 = "hello";
        book.SaveAs(outputPath);
        book.Close(SaveChanges: false);
    }
    finally
    {
        excel.Quit();
    }
}

Für diesen Ansatz gelten drei Voraussetzungen.

  1. Isolieren Sie den Code, der Excel berührt, in einer einzigen Methode. Solange der JIT eine Referenz auf dem Stack am Leben hält, kann der GC sie nicht einsammeln, daher muss der GC-Aufruf immer außerhalb der Methode erfolgen, die Excel berührt hat. NoInlining existiert, damit Inlining diese Isolation nicht aufhebt.
  2. Lassen Sie niemals einen RCW in ein Feld oder einen Rückgabewert entweichen. Entkommt auch nur einer außerhalb der Methode, bleibt Excel so lange am Leben wie diese Referenz.
  3. Verwenden Sie die Dreierkombination GC.CollectGC.WaitForPendingFinalizersGC.Collect. Da die RCW-Bereinigung über einen Finalizer geschieht, ist das Standardmuster ein zweistufiger Durchlauf: der erste Collect erkennt es, Sie warten auf den Abschluss des Finalizers, und der zweite Collect räumt den Rest auf.

Ein Vorbehalt: Ist ein Debugger angehängt, wird die Lebensdauer einer Variable bis zum Ende der Methode verlängert, sodass selbst dieser Ansatz den RCW nicht einsammeln kann. Das ist die eigentliche Identität hinter dem Phänomen „bleibt unter dem Debugger bestehen, verschwindet aber im Release“ — überprüfen Sie das Verhalten immer mit einem Release-Build ohne angehängten Debugger.

Der Vorteil dieses Ansatzes ist, dass das Brechen der Zwei-Punkt-Regel kein Leck verursacht. Alles, dessen Referenz den Gültigkeitsbereich verlassen hat — einschließlich anonymer Zwischen-RCWs — wird gemeinsam vom GC eingesammelt. Da sich das, was ein Reviewer prüfen muss, auf eine einzige Frage reduziert — „ist die Excel berührende Arbeit auf diese Methode beschränkt?“ — sinken die Disziplinkosten drastisch. Die Nachteile sind der Code-Geruch eines expliziten GC.Collect-Aufrufs (eine vollständige, app-weite GC-Pause) und das Risiko, dass ein gutgemeintes Refactoring durch jemanden, der den Grund dafür nicht kennt, dies zerstört. Hinterlassen Sie immer einen Kommentar, der die Dreierkombination erklärt.

4.3 Unsere Empfehlung — standardmäßig Isolation + GC, ein Wrapper bei Bedarf für Disziplin

Vergleicht man die beiden Schulen aus praktischer Sicht:

  (a) ReleaseComObject-Schule (b) GC-Schule
Zeitpunkt der Freigabe Deterministisch (der Moment des Aufrufs) Halb-deterministisch (der Moment der Dreier-GC-Kombination)
Disziplinkosten Hoch. Alle müssen konsequent jedes Objekt in eine Variable verwandeln und in umgekehrter Reihenfolge freigeben Niedrig. Nur die Methodenisolation muss aufrechterhalten werden
Symptome bei Missbrauch InvalidComObjectException, Zugriffsverletzungen2 Pausen durch erzwungenen GC
Übereinstimmung mit offizieller Anleitung „Nur wenn unbedingt erforderlich“2 Folgt der vorgesehenen Lebensdauerverwaltung des RCW (dem GC überlassen)1
Gut geeignet für Wenn die Freigabereihenfolge zählt. Langlebige Prozesse, die Excel wiederholt in kleinen Schüben berühren Die meisten Berichtserzeugungsarbeiten, bei denen „öffnen, schreiben, schließen“ an einer Stelle abgeschlossen werden kann

Unsere Empfehlung lautet, standardmäßig auf das Muster Isolation-plus-GC (b) zu setzen. Arbeit wie die Berichtserzeugung passt von Natur aus in eine einzige Methode, und ist sie erst einmal so geformt, können Lecks strukturell einfach nicht auftreten. Das vermeidet auch das Risiko eines Missbrauchs von ReleaseComObject und deckt sich damit, wie die offizielle Dokumentation es positioniert.

Reservieren Sie (a) für Fälle, in denen Freigabereihenfolge und Sofortigkeit wirklich zählen — und lassen Sie auch dort keine rohen ReleaseComObject-Aufrufe schreiben; führen Sie stattdessen einen kleinen using-basierten Wrapper ein, der die Freigabe diszipliniert.

using System.Runtime.InteropServices;

/// <summary>Wrapper, der ein COM-Objekt am Ende eines using-Bereichs freigibt</summary>
public readonly struct ComScope<T> : IDisposable where T : class
{
    public T Value { get; }
    public ComScope(T value) => Value = value;

    public void Dispose()
    {
        if (Value is not null && Marshal.IsComObject(Value))
            Marshal.ReleaseComObject(Value);
    }
}
using var books = new ComScope<Excel.Workbooks>(excel.Workbooks);
using var book  = new ComScope<Excel.Workbook>(books.Value.Open(templatePath));
using var sheets = new ComScope<Excel.Sheets>(book.Value.Worksheets);
using var sheet = new ComScope<Excel.Worksheet>((Excel.Worksheet)sheets.Value[1]);
using var cell  = new ComScope<Excel.Range>(sheet.Value.Range["A1"]);
cell.Value.Value2 = "hello";
book.Value.SaveAs(outputPath);
book.Value.Close(SaveChanges: false);

Dispose läuft in umgekehrter Reihenfolge der using-Deklarationen ab, sodass „in umgekehrter Erzeugungsreihenfolge freigeben“ durch den Sprachmechanismus selbst garantiert wird. Das Schreiben kostet durch den .Value-Zugriff etwas mehr, doch die Disziplin verdichtet sich auf eine einzige Regel: „jedes COM-Objekt muss über einen ComScope erfasst werden.“ Schreiben Sie umgekehrt einen zusammengesetzten Ausdruck wie books.Value.Open(...).Worksheets, bringt das das Leck sofort zurück, weshalb das Vermitteln der Zwei-Punkt-Regel so oder so nötig bleibt.

Zwei Vorbehalte gelten für beide Muster. Erstens: Lassen Sie keinen Speicherbestätigungsdialog aufpoppen, bevor Quit() aufgerufen wird — setzen Sie DisplayAlerts = false und rufen Sie explizit Close(SaveChanges: false) auf. Hängt eine versteckte Excel-Instanz wartend an einem Dialog fest, wird Quit() selbst nie abgeschlossen. Zweitens: Da Excels COM um STA herum aufgebaut ist, reichen Sie niemals dieselbe Application-Instanz über mehrere Threads hinweg herum. Die Beziehung zwischen Threads und COM-Apartments wird in „STA/MTA-Grundlagen für COM“ behandelt.

5. Das letzte Mittel — zuverlässige Bereinigung durch Identifizierung der PID über das Hwnd

Selbst mit einem korrekt implementierten Freigabemuster bleiben Fälle bestehen, in denen „Excel wegen eines Add-ins nicht beendet“ oder „ein abnormaler Ausnahmepfad hinterlässt gelegentlich einen Prozess“. In einem unbeaufsichtigten Batch-Job kann dieser eine verbleibende Prozess eine Dateisperre für den Job des nächsten Tages verursachen, sodass es sich lohnt, das Beenden eines Prozesses als letztes Sicherheitsnetz einzubauen. Die verbleibende Frage ist, wie man identifiziert, „welches EXCEL.EXE zu beenden ist“.

Ein häufiger Fehler ist die Identifizierung durch Vergleich der Prozessliste vor und nach dem Start — der Vergleich von Process.GetProcessesByName("EXCEL") vorher und nachher und die Behandlung des Zuwachses als eigene Instanz. Das ist anfällig gegenüber Nebenläufigkeit: Öffnet ein Benutzer Excel von Hand, während Sie den Vergleich durchführen, erhalten Sie ein Falsch-Positiv, und läuft dieselbe Art von Job parallel, verwechseln die Jobs gegenseitig ihre Prozesse. Der schlimmste Unfall mit dieser Methode ist, eine ungespeicherte Excel-Instanz zu beenden, die der Benutzer gerade aktiv bearbeitet, und dessen Daten zu verlieren — etwas, das uns tatsächlich als realer Beratungsfall zugetragen wurde.

Die korrekte Methode besteht darin, das oberste Fenster-Handle aus der Hwnd-Eigenschaft des selbst gestarteten Application-Objekts zu holen und mit der Win32-API GetWindowThreadProcessId die ID des Prozesses zu ermitteln, der dieses Fenster erzeugt hat.34 Das Handle ist eindeutig der eigenen Instanz zugeordnet, sodass es keinen Raum für eine Verwechslung mit einem anderen Excel-Prozess gibt.

using System.Diagnostics;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;

internal static class NativeMethods
{
    [DllImport("user32.dll")]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
}

public void ExportReport(string templatePath, string outputPath)
{
    // Die Verwendung eines out-Parameters sorgt dafür, dass das Handle auch dann
    // bis zum Aufrufer erhalten bleibt, wenn mitten in den Excel-Operationen eine
    // Ausnahme geworfen wird, sodass GC und Kill auch auf dem Ausnahmepfad erreicht
    // werden (genau dort, wo dieses Sicherheitsnetz tatsächlich gebraucht wird)
    Process excelProcess = null;
    try
    {
        ExportReportCore(templatePath, outputPath, out excelProcess);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        if (excelProcess != null)
        {
            KillIfStillAlive(excelProcess);   // Ein wirkungsloses Sicherheitsnetz, falls es sich schon normal beendet hat
            excelProcess.Dispose();
        }
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath, out Process excelProcess)
{
    excelProcess = null;
    var excel = new Excel.Application();
    // Dies unmittelbar nach erfolgreichem Start in try/finally einschließen, damit
    // auch dann immer Quit() erreicht wird, wenn das Holen des Hwnd oder das
    // Öffnen des Handles fehlschlägt
    try
    {
        // Dies gleich nach dem Start erfassen, da das Fenster nach Quit()
        // verschwindet und nicht mehr abrufbar ist. GetProcessById bildet nur die
        // PID ab; das OS-Prozess-Handle wird erst beim ersten Zugriff verzögert
        // geöffnet — etwa durch WaitForExit / Kill. Deshalb hier, während Excel
        // noch lebt, SafeHandle anfassen, um das Handle sofort zwangsweise zu
        // öffnen (solange das Handle offen bleibt, wird diese PID nicht von
        // einem anderen Prozess wiederverwendet)
        NativeMethods.GetWindowThreadProcessId((IntPtr)excel.Hwnd, out uint pid);
        excelProcess = Process.GetProcessById((int)pid);
        _ = excelProcess.SafeHandle;

        excel.DisplayAlerts = false;
        // …… Excel-Operationen ……
    }
    finally
    {
        excel.Quit();
    }
}

private void KillIfStillAlive(Process excelProcess)
{
    if (!excelProcess.WaitForExit(5000))   // Normalerweise verschwindet es innerhalb weniger Sekunden
    {
        logger.LogWarning("EXCEL.EXE (PID {Pid}) hat sich nicht beendet, wird zwangsweise beendet", excelProcess.Id);
        excelProcess.Kill();
    }
}

Hinweise zur Implementierung:

  • Erfassen Sie Hwnd unmittelbar nach dem Start. Nach Quit() wird das Fenster zerstört und kann nicht mehr abgerufen werden. Application.Hwnd lässt sich auch mit Visible = false ermitteln.3
  • Kill ist ein Sicherheitsnetz zusätzlich zur korrekten Freigabe, kein Ersatz dafür. Sich von Anfang an darauf zu verlassen bedeutet, dass Excels Bereinigung temporärer Dateien nie läuft, was beim nächsten Start zu einer Ansammlung von Wiederherstellungsdateien führen kann. Die Reihenfolge muss immer „freigeben → Quit → warten → nur bei fortbestehendem Leben Kill“ sein.
  • Achten Sie auf PID-Wiederverwendung. Da Windows-PIDs wiederverwendet werden, riskiert ein Entwurf, der sich nur die PID-Nummer merkt und sie später auflöst, einen unbeteiligten Prozess zu beenden, falls Excel sich in der Zwischenzeit beendet und dieselbe PID an etwas anderes vergeben wird. Beachten Sie, dass der bloße Aufruf von Process.GetProcessById hier allein nicht ausreicht — das OS-Prozess-Handle wird erst beim ersten Zugriff verzögert geöffnet, etwa durch Aufrufe wie WaitForExit / Kill. Nur indem Sie, wie im obigen Code, SafeHandle anfassen, während Excel noch lebt, um das Handle explizit zwangsweise zu öffnen, verhindern Sie diese Verwechslung tatsächlich (solange das Handle offen bleibt, wird diese PID nicht wiederverwendet).
  • Was dieses Sicherheitsnetz abdeckt, ist der Fall, in dem die Methode bereits zurückgekehrt ist, EXCEL.EXE aber immer noch da ist. Hängt der COM-Aufruf selbstWorkbooks.Open, SaveAs, Quit, wartend auf einen versteckten modalen Dialog oder ein nicht reagierendes Add-in —, wird finally nie erreicht, und auch dieser Kill wird nie ausgelöst. Denken Sie sich die Gegenmaßnahme in zwei Schichten. Zunächst schließen Sie die Dialogquellen mit DisplayAlerts = false und der zuvor beschriebenen AutomationSecurity-Einstellung. Darüber hinaus sollten Sie für unbeaufsichtigte Batch-Jobs den Excel berührenden Job als eigenen separaten Prozess strukturieren, damit er von außen unter einem Zeitlimit vollständig beendet werden kann — die Einstellung „Aufgabe beenden, wenn sie länger läuft als“ der Aufgabenplanung, oder ein Elternprozess, der sein Kind beendet. Ein prozessinternes Timeout kann einen hängenden COM-Aufruf nicht unterbrechen, weshalb es zuverlässiger ist, die Grenze auf Prozessebene zu ziehen.
  • Wann immer Kill auslöst, protokollieren Sie es immer, und überwachen Sie, wie oft das passiert. Eine steigende Häufigkeit ist ein Signal entweder für eine Regression im Freigabecode oder für einen in Abschnitt 7 behandelten Entscheidungspunkt, COM-Interop insgesamt zu ersetzen.

6. Einen Schritt zurück — serverseitige Office-Automatisierung wird nicht unterstützt

Die bisher behandelten Techniken können die EXCEL.EXE-Leckage in einer Client-Anwendung fast vollständig unterdrücken. Doch für das Szenario, in dem dieses Problem am schlimmsten wird — Excel-Automatisierung auf einem Server oder in einer unbeaufsichtigten Umgebung — gibt es eine offizielle Haltung, die zu prüfen sich lohnt, bevor überhaupt eine Technik zum Einsatz kommt.

In einem Dokument mit dem Titel „Considerations for server-side Automation of Office“ erklärt Microsoft unmissverständlich, dass es die Automatisierung von Office aus unbeaufsichtigten, nicht-interaktiven Client-Anwendungen oder -Komponenten — einschließlich ASP, ASP.NET, DCOM und NT-Diensten — weder empfiehlt noch unterstützt.5 Office ist unter der Annahme entworfen, dass ein interaktiver Benutzer vorhanden ist, und Voraussetzungen, die auf einem Desktop keine Probleme verursachen — ein Entwurf, der bei einem Fehler einen Bestätigungsdialog öffnet und auf eine Antwort wartet, Komponenten, die das Profil des ausführenden Benutzers voraussetzen, eine STA-basierte, nicht wiedereintrittsfähige Architektur — werden alle feindselig, sobald sie in einem Dienst oder einem IIS-Arbeitsprozess laufen.9 Fälle, mit denen wir konsultiert wurden — „Open kehrt in der Produktion nie zurück, wenn es aus einem Windows-Dienst läuft“, „es hängt einmal im Monat auf IIS an einem Dialog fest“ — ließen sich allesamt auf diese nicht unterstützte Konfiguration zurückführen. EXCEL.EXE-Leckage in einer solchen Konfiguration ist nicht nur ein Bereinigungsproblem — sie wird zu einem Betriebsproblem, bei dem sich hängende Prozesse unbegrenzt anhäufen.

Was Microsoft als Alternativen nennt, ist das direkte Bearbeiten des Open-XML-Dateiformats, ohne Office zu installieren oder zu starten, sowie die Microsoft-Graph-API, die serverseitig in der Cloud verarbeitet.9 Das Open XML SDK ist eine Microsoft-Bibliothek, die die Dateiformate von Office (etwa .xlsx), standardisiert als ECMA-376 / ISO/IEC 29500, über stark typisierte Klassen liest und schreibt; da es auf ZIP und XML aufbaut, wird Excel selbst nicht benötigt.6 Die Probleme von Prozessleckage, Lizenzierung und nicht unterstützten Konfigurationen verschwinden damit alle auf einmal.

Das Open XML SDK ist jedoch eine Bibliothek, die „das Dateiformat direkt bearbeitet“, und bietet nicht das Verhalten der Excel-Anwendung selbst. Seine offiziellen Entwurfsüberlegungen erklären ausdrücklich, dass es keine Anwendungsverhalten wie das Neuberechnen von Formeln oder das Aktualisieren von Daten bereitstellt, ebenso wenig eine Konvertierung in andere Formate wie PDF.10 Auch die API bleibt der Struktur des Dateiformats treu, sodass selbst das Schreiben einer einzigen Zelle mit dem bloßen SDK ein Verständnis der Struktur von SpreadsheetML erfordert. ClosedXML füllt diese Lücke — eine MIT-lizenzierte OSS-Bibliothek, die über der Open-XML-API eine intuitive API aus „Arbeitsmappe, Blatt, Zelle“ legt. Sie kann .xlsx / .xlsm ohne installiertes Excel handhaben (das ältere .xls-Format liegt außerhalb ihres Umfangs).7 Wie man im Kontext der Berichtserzeugung zwischen diesen Ansätzen wählt und wie man einen vorlagenbasierten Ansatz entwirft, wird ausführlich in „Wie man die Excel-Berichtsausgabe aufbaut“ behandelt.

Beachten Sie, dass Microsoft 365 zwar eine Lizenz für unbeaufsichtigte RPA besitzt, doch dies macht die unbeaufsichtigte Ausführung nur aus lizenzrechtlicher Sicht möglich; das Verhalten wird weiterhin als „AS IS“ eingestuft — jedes unerwartete Verhalten, das aus einem außerhalb des Entwurfs liegenden Anwendungsfall entsteht, muss die Anwendung selbst abfedern.9 Das ist ein häufiger Verwirrungspunkt: Der Kauf der Lizenz macht es nicht zu einer unterstützten Konfiguration.

7. Entscheidungstabelle — COM weiter nutzen oder durch Open XML ersetzen

Angesichts all dessen lässt sich die Frage „Excel weiter per COM betreiben oder davon weggehen“ auf die folgenden drei Optionen herunterbrechen.

  (1) COM-Interop weiter nutzen (gewrappt und diszipliniert) (2) Durch Open XML SDK / ClosedXML ersetzen (3) Den Entwurf vollständig überdenken (Graph usw.)
Excel selbst Erforderlich (und eine Lizenz pro Ausführungsumgebung) Nicht erforderlich Nicht erforderlich
Unbeaufsichtigte / Server-Ausführung Nicht unterstützte Konfiguration5 Kein Problem (die empfohlene Alternative)9 Kein Problem
Risiko von Prozessleckage Vorhanden (verwaltet über die Techniken in diesem Artikel) Keines (startet nie einen Prozess) Keines
Makros (VBA) ausführen Ja Nein (und selbst der Erhalt hängt von der Bibliothek ab — siehe Punkt 2 unten) Nein
Formelneuberechnung, Drucken, PDF-Konvertierung Ja Nein10 Teilweise über Graph verfügbar
Interaktion mit einer vom Benutzer geöffneten Excel-Instanz Ja Nein Nein
Altes .xls-Format (BIFF) Lesen/Schreiben Nein (nur .xlsx / .xlsm)7
Ausführungsgeschwindigkeit / Parallelität Langsam. Instanzisolation für Parallelität nötig9 Schnell. Kann wie eine gewöhnliche Bibliothek parallel laufen Hängt vom Netzwerk ab

Die Entscheidung läuft auf vier Fragen hinaus.

  1. Muss mit der Excel-Instanz interagiert werden, die vor dem Benutzer geöffnet ist? Eine Funktion, die „in eine vom Benutzer geöffnete Arbeitsmappe schreibt und die Kontrolle zurückgibt, damit er weiterarbeiten kann“, lässt sich nur mit COM-Interop bauen. In diesem Fall ist (1) die einzige Option — investieren Sie in die in Abschnitt 4 beschriebene Disziplin. Interaktive Desktop-Apps fallen auch nicht unter die nicht unterstützte Konfiguration.
  2. Werden Fähigkeiten der Excel-Anwendung selbst benötigt — Makros ausführen, Neuberechnung, Drucken, PDF-Ausgabe? Diese lassen sich nicht durch die Open-XML-Familie ersetzen.10 Diese mit unbeaufsichtigter Ausführung zu kombinieren, ist das schwierigste aller Muster, prüfen Sie also zuerst, ob sich die Anforderung selbst umformen lässt — die Logik des Makros nach C# zu portieren, bereits berechnete Werte zu schreiben, und Ähnliches. Ein Punkt, der besonders sorgfältig geprüft werden sollte, ist, ob eine makrofähige Vorlage (.xlsm) tatsächlich „überlebt“. Low-Level-Operationen mit dem Open XML SDK erhalten sie, solange Sie den VBA-Projektteil nicht anfassen, doch eine High-Level-Bibliothek wie ClosedXML lädt die Arbeitsmappe in ein Objektmodell und baut das Paket neu auf und speichert es, was das VBA-Projekt verlieren kann. Migrieren Sie eine makrofähige Vorlage in die Open-XML-Familie, machen Sie es zur Pflicht, an der echten Vorlage zu verifizieren, „dass das Makro nach dem Öffnen, Schreiben und Speichern noch vorhanden ist und funktioniert“ — und können Sie das nicht garantieren, belassen Sie diesen bestimmten Bericht bei COM. Für den Umgang mit VBA-Beständen gilt der Entscheidungsrahmen in „Was sind die Grenzen von VBA?“ auch hier unmittelbar.
  3. Läuft es unbeaufsichtigt? Läuft es aus einem Dienst, der Aufgabenplanung oder einer Web-App, lautet die Standardantwort (2). Der Großteil der Anforderungen an Berichtserzeugung läuft auf nichts weiter als „eine mit Werten und Formatierungen gefüllte .xlsx erzeugen“ hinaus, und das lässt sich mit ClosedXML vollständig erreichen.
  4. In welchem Format liegen Eingabe/Ausgabe vor? Müssen eingehende .xls-Dateien eines Geschäftspartners unverändert verarbeitet werden, scheidet die Open-XML-Familie aus. Erwägen Sie, ob sich stattdessen an der Aufnahmestelle eine Konvertierung nach .xlsx einfügen lässt.

Was wir in echten Projekten oft vorschlagen, ist eine Aufteilung: mit ClosedXML erzeugen, und nur die Arbeit, die wirklich Excel selbst benötigt, in COM isolieren. Führen Sie die tägliche Erzeugung Hunderter Berichte serverseitig mit ClosedXML aus, und reservieren Sie COM-Verarbeitung — ein Klick auf dem Desktop der verantwortlichen Person — nur für die monatliche Aufgabe „die makrofähige Arbeitsmappe aktualisieren“. Das entfernt COM vollständig aus der unbeaufsichtigten Umgebung, und der verbleibende COM-Teil ist eine interaktive App, fällt also unter eine unterstützte Konfiguration. Das ist realistischer als eine vollständige Neuschreibung, und es erlaubt, Risiko beginnend beim riskantesten Teil auszuräumen.

8. Hinweise für die Ära von .NET (Core)

Hier ein Überblick, soweit wir es verifizieren konnten, worauf zu achten ist, wenn Excel-COM-Operationen in einer App fortgeführt werden, die von .NET Framework nach .NET (.NET 6/8 usw.) migriert wurde.

  • COM-Interop bleibt nur unter Windows verfügbar. .NET läuft auch unter Linux, doch seine eingebaute COM-Interop-Unterstützung ist auf Windows beschränkt.11 Erklären Sie für ein Projekt mit Excel-Operationen explizit ein Ziel wie net8.0-windows, und erwarten Sie keine plattformübergreifende Unterstützung. Dass es sich nicht in einem Linux-Container hosten lässt, hängt unmittelbar mit der Ersatzentscheidung aus Abschnitt 7 zusammen (ClosedXML dagegen läuft durchaus in einem Linux-Container).
  • Der Standardansatz für Referenzen ist „COM-Referenz plus eingebettete Interop-Typen“. Fügen Sie in Visual Studio die Microsoft Excel Object Library als COM-Referenz hinzu, wird standardmäßig Embed Interop Types verwendet. Nur die tatsächlich genutzten Typen werden in die eigene Assembly eingebettet, sodass keine PIA (Primary Interop Assembly) in die Laufzeitumgebung ausgeliefert werden muss, und es ist widerstandsfähiger gegenüber Office-Versionsunterschieden.12
  • dynamic und optionale Argumente funktionieren weiterhin. Die für Office-Interop gebauten C#-Funktionen — benannte und optionale Argumente sowie die Vereinfachung von COM-Aufrufen mit dynamic — bleiben im aktuellen .NET unterstützt.12 Allerdings macht das Schreiben mit dynamic anonyme RCWs noch schwerer erkennbar, weshalb wir für Code, in dem eine disziplinierte Freigabe gewünscht ist, empfehlen, stattdessen mit expliziten Typen zu schreiben.
  • Achten Sie auf plattformspezifische APIs. COM-bezogene APIs wie Marshal.ReleaseComObject tragen ein Nur-Windows-Attribut, und ihr Aufruf aus einem plattformübergreifenden Projekt löst eine Analyzer-Warnung aus (CA1416). Excel-Operationen in ein eigenes, separates Projekt auszulagern, macht dies leichter handhabbar.
  • Die umgekehrte Richtung — .NET aus VBA aufrufen — ist eine andere Geschichte. Ein Aufbau, bei dem eine .NET-8-DLL als COM offengelegt und aus VBA konsumiert wird, bleibt möglich; das Vorgehen wird in „Wie man eine .NET-8-DLL mit Typinformationen aus VBA konsumiert“ behandelt. Dreht man die Architektur um — statt „Excel aus C# steuern“ lässt man „das Makro von Excel C#-Logik aufrufen“ —, lässt man Excel selbst die Lebensdauer des Prozesses verwalten, und in manchen Fällen verschwindet das Leckageproblem dadurch strukturell.

Kurz gesagt: Auch in der Ära von .NET (Core) sind die Art, Excel-COM-Operationen zu schreiben, und die damit verbundenen Fallen fast identisch mit der Ära von .NET Framework. Was sich geändert hat, ist, dass Sie nun den Nur-Windows-Status explizit erklären und sich Referenzen hin zu eingebetteten Interop-Typen verschoben haben; die Besprechung von RCWs und Freigabemustern (Abschnitte 2–5) gilt genauso wie zuvor.

9. Zusammenfassung

Das Problem, dass EXCEL.EXE am Leben bleibt, hört auf, eine Frage des Zufalls zu sein, sobald man die Brücke zwischen zwei unterschiedlichen Lebensdauerverwaltungssystemen versteht: „COM lebt nach Referenzzählung, und der RCW von .NET stirbt über den GC.“ Zusammengefasst als Checkliste:

  • Quit() ist keine Freigabe. Excel beendet sich erst, wenn jede von einem RCW gehaltene COM-Referenz zurückgegeben wurde.
  • Ein zusammengesetzter Ausdruck mit zwei oder mehr Punkten erzeugt einen anonymen RCW. Erfassen Sie Zwischenobjekte in Variablen.
  • Setzen Sie zur Freigabe standardmäßig auf „Methodenisolation plus die Dreier-GC-Kombination“; wird die Reihenfolge wirklich benötigt, disziplinieren Sie ReleaseComObject mit einem using-Wrapper. Verstreuen Sie keine rohen ReleaseComObject-Aufrufe.
  • Identifizieren Sie für den Sicherheitsnetz-Kill nur die eigene Instanz über Application.HwndGetWindowThreadProcessId, und lösen Sie ihn nur unter „Quit → warten → Timeout“ aus.
  • Serverseitige, unbeaufsichtigte Excel-Automatisierung ist eine nicht unterstützte Konfiguration. Verlagern Sie unbeaufsichtigte Berichtserzeugung nach ClosedXML / Open XML SDK, und beschränken Sie Arbeit, die Makros oder Neuberechnung benötigt, auf COM in einer interaktiven Umgebung.

Finden Sie eine Reihe von EXCEL.EXE-Einträgen im Task-Manager, ist das ein Zeichen entweder für ein Technikproblem (Abschnitte 4–5) oder ein Konfigurationsproblem (Abschnitte 6–7). Sind Sie unsicher, in welche Kategorie Ihr Code fällt, oder wo Sie anfangen sollen, falls Sie sich für einen Ersatz entscheiden, können wir helfen, beginnend mit einer Inventarisierung der betreffenden Verarbeitung.

Verwandte Artikel

Verwandte Beratungsbereiche

Die Komura Soft LLC übernimmt die Fehlersuche bei Windows-Apps mit Excel-/Office-Automatisierung (Prozessleckage, Hänger, Dateisperren), die Pflege und Disziplinierung von COM-Beständen sowie den Entwurf von Migrationen der Berichtsverarbeitung zu Bibliotheken der Open-XML-Familie.

  1. Microsoft Learn, Runtime Callable Wrapper. Dazu, wie pro COM-Objekt innerhalb eines Prozesses genau ein RCW erzeugt wird, wie er den Schnittstellenzeiger cacht, und wie er seine Referenz auf das COM-Objekt freigibt, wenn er vom GC eingesammelt wird.  2 3 4

  2. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. Dazu, wie es den Referenzzähler des RCW verringert, zum Risiko von InvalidComObjectException, Zugriffsverletzungen und Speicherbeschädigung durch die Nutzung eines bereits freigegebenen RCW, zu seiner Positionierung als etwas, das „nur verwendet werden sollte, wenn unbedingt erforderlich“, und zu seiner Beziehung zu FinalReleaseComObject.  2 3 4 5 6 7

  3. Microsoft Learn, Application.hWnd property (Excel). Dazu, wie die Hwnd-Eigenschaft des Application-Objekts von Excel das Handle seines obersten Fensters zurückgibt.  2 3

  4. Microsoft Learn, GetWindowThreadProcessId function (winuser.h). Dazu, wie sie die ID des Threads ermittelt, der ein gegebenes Fenster erzeugt hat, sowie die ID des Prozesses, der es erzeugt hat.  2

  5. Microsoft Support, Considerations for server-side Automation of Office. Dazu, dass Microsoft die Office-Automatisierung aus unbeaufsichtigten, nicht-interaktiven Client-Anwendungen oder -Komponenten — einschließlich ASP, ASP.NET, DCOM und NT-Diensten — weder empfiehlt noch unterstützt.  2 3

  6. Microsoft Learn, Welcome to the Open XML SDK for Office. Dazu, dass das Open XML SDK eine auf System.IO.Packaging basierende Bibliothek ist, die als ECMA-376 / ISO/IEC 29500 standardisierte Office-Dateiformate über stark typisierte Klassen bearbeitet.  2

  7. GitHub, ClosedXML/ClosedXML. Dazu, dass es eine MIT-lizenzierte Bibliothek ist, die über der Open-XML-API eine intuitive Schnittstelle bereitstellt und Excel-2007+-Dateien (.xlsx, .xlsm) ohne installiertes Excel handhaben kann.  2 3

  8. Microsoft Learn, _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). Zum Makro-Sicherheitsmodus, der beim programmgesteuerten Öffnen einer Datei verwendet wird, dazu, dass der Standard beim Anwendungsstart msoAutomationSecurityLow ist (alle Makros aktiviert), und dazu, wie msoAutomationSecurityForceDisable alle Makros ohne Warnung deaktiviert. 

  9. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Zu den Problemen mit interaktiver UI, Benutzeridentifikation und dem Single-Thread-STA-Entwurf bei unbeaufsichtigter Automatisierung, dazu, dass das Verhalten auch unter einer unbeaufsichtigten Lizenz AS IS bleibt, und dazu, dass Microsoft Graph sowie das direkte Bearbeiten des Open-XML-Dateiformats als Alternativen empfohlen werden.  2 3 4 5

  10. Microsoft Learn, Open XML SDK for Office design considerations. Dazu, dass das Open XML SDK kein Ersatz für das Office-Objektmodell ist und keine Anwendungsverhalten wie Formelneuberechnung oder Datenaktualisierung bereitstellt, ebenso wenig eine Konvertierung in andere Formate.  2 3

  11. Microsoft Learn, Native interoperability ABI support. Dazu, dass die Unterstützung für das eingebaute COM-Interop-System auf Windows beschränkt ist, sowie zur COM-Unterstützung über ComWrappers in .NET 5+ und Quellcodegenerierung in .NET 8+. 

  12. Microsoft Learn, How to access Office interop objects. Zur Vereinfachung von Office-Interop durch benannte Argumente, optionale Argumente und dynamic, sowie dazu, dass Embed Interop Types anstelle einer PIA das Standardverhalten ist.  2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Warum läuft EXCEL.EXE weiter, obwohl ich Quit() aufrufe?
Quit() ist nur eine Bitte, die besagt: „Beende dich, sobald jede Referenz freigegeben wurde“ — es gibt selbst nichts frei. .NET-Code bedient COM-Objekte über Runtime Callable Wrapper (RCWs), und jeder RCW hält seine COM-Referenz so lange, bis er vom Garbage Collector eingesammelt oder explizit freigegeben wird. Da der Zeitpunkt der Freigabe davon abhängt, wann der GC zufällig läuft, kann EXCEL.EXE zig Minuten herumstehen, weshalb sich das Symptom unstet anfühlt — es verschwindet auf der Entwicklungsmaschine, bleibt aber in der Produktion bestehen, oder bleibt unter dem Debugger bestehen, verschwindet aber im Release-Build.
Was ist die Zwei-Punkt-Regel bei Excel-COM-Interop?
Verketten Sie niemals zwei oder mehr Punkte an einem COM-Objekt — erfassen Sie jedes Zwischenobjekt zuerst in einer Variable. Ein Ausdruck wie book.Worksheets[1].Range["A1"] erzeugt RCWs für Worksheets, Worksheets[1] und Range["A1"], die nie einer Variable zugewiesen werden, sodass sie nie explizit freigegeben werden können und Excel am Leben halten. Dieselbe Falle versteckt sich in foreach-Schleifen über COM-Sammlungen, in Wegwerfausdrücken innerhalb von Bedingungen wie if (excel.Workbooks.Count > 0), in zusammengesetzten Argumenten und in Ereignisabonnements, die nie abbestellt werden.
Sollte ich Marshal.ReleaseComObject verwenden oder mich auf den Garbage Collector verlassen?
Die empfohlene Standardeinstellung ist das GC-Muster: Isolieren Sie die Excel-berührende Arbeit vollständig in einer einzigen Methode (mit NoInlining markiert), lassen Sie keinen RCW in ein Feld oder einen Rückgabewert entweichen, und führen Sie nach der Rückkehr der Methode in einem finally-Block GC.Collect, GC.WaitForPendingFinalizers und dann erneut GC.Collect aus. Das ist strukturell leckfrei, selbst wenn jemand die Zwei-Punkt-Regel bricht, und es passt zur offiziellen Dokumentation, die besagt, ReleaseComObject „nur zu verwenden, wenn es unbedingt erforderlich ist“ — der falsche Einsatz bei einem gemeinsam genutzten RCW riskiert InvalidComObjectException oder sogar Speicherbeschädigung. Reservieren Sie die explizite Freigabe für Fälle, in denen die Freigabereihenfolge wirklich zählt, und disziplinieren Sie das auch dann eher über einen kleinen using-basierten Wrapper, statt rohe ReleaseComObject-Aufrufe zu verstreuen.
Ist es unterstützt, Excel aus einem Windows-Dienst oder einer Web-App zu automatisieren?
Nein. Microsoft erklärt unmissverständlich, dass es die Automatisierung von Office aus unbeaufsichtigten, nicht-interaktiven Anwendungen — einschließlich ASP.NET, DCOM und NT-Diensten — weder empfiehlt noch unterstützt, weil Office einen interaktiven Benutzer voraussetzt und Dinge tut wie endlos auf einen Bestätigungsdialog zu warten, den niemand sehen kann. Die empfohlenen Alternativen sind das direkte Bearbeiten des Open-XML-Dateiformats (über das Open XML SDK oder die zugänglichere Bibliothek ClosedXML, von denen keine Excel überhaupt startet) oder die Microsoft-Graph-API. Beachten Sie, dass die Microsoft-365-Lizenz für unbeaufsichtigte RPA die unbeaufsichtigte Ausführung nur rechtlich zulässig macht — sie macht sie nicht zu einer unterstützten Konfiguration.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog