From ce0f127b49ac3b94578f81d6b9fd09b704fecc51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:00:40 +0000 Subject: [PATCH 01/10] Initial plan From 7befd6a20d99fa40d6e0b3d078c74c0e2a899141 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:07:07 +0000 Subject: [PATCH 02/10] Add comprehensive COM object cleanup documentation and examples Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> --- .../interop/snippets/OfficeInterop/program.cs | 140 ++++++++++++++---- .../snippets/OfficeWalkthrough/ThisAddIn.cs | 117 ++++++++++++++- .../interop/walkthrough-office-programming.md | 46 ++++++ 3 files changed, 274 insertions(+), 29 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index e2a062005586c..a610b882b133b 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; // using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; @@ -48,39 +49,128 @@ static void Main(string[] args) // static void DisplayInExcel(IEnumerable accounts) { - var excelApp = new Excel.Application(); - // Make the object visible. - excelApp.Visible = true; + Excel.Application excelApp = null; + Excel.Workbook workbook = null; + Excel.Worksheet workSheet = null; + + try + { + excelApp = new Excel.Application(); + // Make the object visible. + excelApp.Visible = true; - // Create a new, empty workbook and add it to the collection returned - // by property Workbooks. The new workbook becomes the active workbook. - // Add has an optional parameter for specifying a particular template. - // Because no argument is sent in this example, Add creates a new workbook. - excelApp.Workbooks.Add(); + // Create a new, empty workbook and add it to the collection returned + // by property Workbooks. The new workbook becomes the active workbook. + // Add has an optional parameter for specifying a particular template. + // Because no argument is sent in this example, Add creates a new workbook. + workbook = excelApp.Workbooks.Add(); - // This example uses a single workSheet. The explicit type casting is - // removed in a later procedure. - Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet; + // This example uses a single workSheet. The explicit type casting is + // removed in a later procedure. + workSheet = (Excel.Worksheet)excelApp.ActiveSheet; + + // Establish column headings in cells A1 and B1. + workSheet.Cells[1, "A"] = "ID Number"; + workSheet.Cells[1, "B"] = "Current Balance"; + + var row = 1; + foreach (var acct in accounts) + { + row++; + workSheet.Cells[row, "A"] = acct.ID; + workSheet.Cells[row, "B"] = acct.Balance; + } + + workSheet.Columns[1].AutoFit(); + workSheet.Columns[2].AutoFit(); + + // Put the spreadsheet contents on the clipboard. + workSheet.Range["A1:B3"].Copy(); + + // Save the workbook before closing + string fileName = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "BankAccounts.xlsx"); + workbook.SaveAs(fileName); + } + finally + { + // Clean up COM objects in reverse order of creation + if (workSheet != null) + { + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workSheet); + workSheet = null; + } + if (workbook != null) + { + workbook.Close(true); // Save changes + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workbook); + workbook = null; + } + if (excelApp != null) + { + excelApp.DisplayAlerts = true; + excelApp.Quit(); + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excelApp); + excelApp = null; + } + + // Force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } // // static void CreateIconInWordDoc() { - var wordApp = new Word.Application(); - wordApp.Visible = true; - - // The Add method has four reference parameters, all of which are - // optional. Visual C# allows you to omit arguments for them if - // the default values are what you want. - wordApp.Documents.Add(); - - // PasteSpecial has seven reference parameters, all of which are - // optional. This example uses named arguments to specify values - // for two of the parameters. Although these are reference - // parameters, you do not need to use the ref keyword, or to create - // variables to send in as arguments. You can send the values directly. - wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); + Word.Application wordApp = null; + Word.Document document = null; + + try + { + wordApp = new Word.Application(); + wordApp.Visible = true; + + // The Add method has four reference parameters, all of which are + // optional. Visual C# allows you to omit arguments for them if + // the default values are what you want. + document = wordApp.Documents.Add(); + + // PasteSpecial has seven reference parameters, all of which are + // optional. This example uses named arguments to specify values + // for two of the parameters. Although these are reference + // parameters, you do not need to use the ref keyword, or to create + // variables to send in as arguments. You can send the values directly. + wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); + + // Save the document + string fileName = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "BankAccountsLink.docx"); + document.SaveAs(fileName); + } + finally + { + // Clean up COM objects in reverse order of creation + if (document != null) + { + document.Close(true); // Save changes + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(document); + document = null; + } + if (wordApp != null) + { + wordApp.Quit(true); // Save changes to all documents + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp); + wordApp = null; + } + + // Force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } // diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs index fc559caf33d22..be730c8f21a21 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs @@ -1,6 +1,7 @@ using System; // using System.Collections.Generic; +using System.Runtime.InteropServices; using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; // @@ -43,10 +44,41 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e) // // - var wordApp = new Word.Application(); - wordApp.Visible = true; - wordApp.Documents.Add(); - wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true); + Word.Application wordApp = null; + Word.Document document = null; + + try + { + wordApp = new Word.Application(); + wordApp.Visible = true; + document = wordApp.Documents.Add(); + wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true); + + // Save the document + string fileName = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "BankAccountsLink.docx"); + document.SaveAs(fileName); + } + finally + { + // Clean up COM objects + if (document != null) + { + document.Close(true); + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(document); + document = null; + } + if (wordApp != null) + { + wordApp.Quit(true); + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp); + wordApp = null; + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); + } // } @@ -72,6 +104,83 @@ void DisplayInExcel(IEnumerable accounts, } // + // + static void CleanupComObject(object comObject) + { + if (comObject != null) + { + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(comObject); + comObject = null; + } + } + // + + // + void DisplayInExcelWithCleanup(IEnumerable accounts, + Action DisplayFunc) + { + Excel.Application excelApp = null; + Excel.Workbook workbook = null; + Excel.Worksheet worksheet = null; + + try + { + excelApp = new Excel.Application(); + excelApp.Visible = true; + + // Add a new Excel workbook. + workbook = excelApp.Workbooks.Add(); + worksheet = workbook.ActiveSheet; + + worksheet.Cells[1, 1].Value = "ID"; + worksheet.Cells[1, 2].Value = "Balance"; + + int row = 2; + foreach (var ac in accounts) + { + var cell = worksheet.Cells[row, 1]; + DisplayFunc(ac, cell); + row++; + } + + // Save the workbook + string fileName = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "BankAccounts.xlsx"); + workbook.SaveAs(fileName); + + // Copy the results to the Clipboard. + worksheet.Range["A1:B3"].Copy(); + } + finally + { + // Always clean up COM objects in reverse order of creation + if (worksheet != null) + { + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(worksheet); + worksheet = null; + } + if (workbook != null) + { + workbook.Close(true); // Save changes + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workbook); + workbook = null; + } + if (excelApp != null) + { + excelApp.DisplayAlerts = true; + excelApp.Quit(); + System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excelApp); + excelApp = null; + } + + // Force garbage collection to help release any remaining references + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + // + private void ThisAddIn_Shutdown(object sender, System.EventArgs e) { diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index f24beeb243e6d..d72424b573758 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -96,6 +96,52 @@ This code demonstrates several of the features in C#: the ability to omit the `r Press F5 to run the application. Excel starts and displays a table that contains the information from the two accounts in `bankAccounts`. Then a Word document appears that contains a link to the Excel table. +## Important: COM object cleanup and resource management + +The examples shown above demonstrate basic Office Interop functionality, but they don't include proper cleanup of COM objects. This is a critical issue in production applications because failing to properly release COM objects can result in orphaned Office processes that remain in memory even after your application closes. + +### Why COM object cleanup is necessary + +COM objects in Office Interop require explicit cleanup because: + +- The .NET garbage collector doesn't automatically release COM objects +- Each Excel or Word object you create holds resources that must be manually released +- Without proper cleanup, Office applications remain running in the background +- This applies to all COM objects: Application, Workbooks, Worksheets, Ranges, and more + +### Proper cleanup pattern + +Use the following pattern to ensure all COM objects are properly released: + +:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="ProperCleanup"::: + +Add the following enhanced version of the `DisplayInExcel` method that includes proper COM object cleanup: + +:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="DisplayWithCleanup"::: + +This pattern ensures that: + +- COM objects are released even if an exception occurs +- Excel processes don't remain orphaned in Task Manager +- Memory is properly freed +- The application behaves reliably in production environments + +For production applications, always implement this cleanup pattern for every COM object you create, including Application, Workbooks, Worksheets, Ranges, and other Office objects. + +### Common questions about COM object cleanup + +**Why can't garbage collection handle this automatically?** +COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. + +**Why must I set objects to null after calling Marshal.FinalReleaseComObject?** +Setting references to null ensures that your code can't accidentally use a released COM object, which would throw an exception. It also helps the garbage collector by removing any remaining managed references. + +**Why call GC.Collect() and GC.WaitForPendingFinalizers()?** +These calls force immediate garbage collection, which helps ensure that any remaining RCWs are cleaned up promptly. While not always strictly necessary, they provide additional safety in COM interop scenarios. + +**What happens if I don't follow this pattern?** +Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. + ## Clean up the completed project In Visual Studio, select **Clean Solution** on the **Build** menu. Otherwise, the add-in runs every time that you open Excel on your computer. From 69cdbfce22e3ead28e930d09c60d780b27014512 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:17:53 +0000 Subject: [PATCH 03/10] Fix COM object cleanup pattern based on @jkotas feedback - use separate non-inlineable methods for reliable object lifetime management Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../interop/snippets/OfficeInterop/program.cs | 36 +++++----- .../snippets/OfficeWalkthrough/ThisAddIn.cs | 72 +++++++++++++------ .../interop/walkthrough-office-programming.md | 7 +- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index a610b882b133b..ffba093ff66ba 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // using Excel = Microsoft.Office.Interop.Excel; @@ -48,6 +49,12 @@ static void Main(string[] args) // static void DisplayInExcel(IEnumerable accounts) + { + DisplayInExcelCore(accounts); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DisplayInExcelCore(IEnumerable accounts) { Excel.Application excelApp = null; Excel.Workbook workbook = null; @@ -98,32 +105,31 @@ static void DisplayInExcel(IEnumerable accounts) // Clean up COM objects in reverse order of creation if (workSheet != null) { - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workSheet); - workSheet = null; + Marshal.FinalReleaseComObject(workSheet); } if (workbook != null) { workbook.Close(true); // Save changes - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workbook); - workbook = null; + Marshal.FinalReleaseComObject(workbook); } if (excelApp != null) { excelApp.DisplayAlerts = true; excelApp.Quit(); - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excelApp); - excelApp = null; + Marshal.FinalReleaseComObject(excelApp); } - - // Force garbage collection - GC.Collect(); - GC.WaitForPendingFinalizers(); } } // // static void CreateIconInWordDoc() + { + CreateIconInWordDocCore(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void CreateIconInWordDocCore() { Word.Application wordApp = null; Word.Document document = null; @@ -157,19 +163,13 @@ static void CreateIconInWordDoc() if (document != null) { document.Close(true); // Save changes - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(document); - document = null; + Marshal.FinalReleaseComObject(document); } if (wordApp != null) { wordApp.Quit(true); // Save changes to all documents - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp); - wordApp = null; + Marshal.FinalReleaseComObject(wordApp); } - - // Force garbage collection - GC.Collect(); - GC.WaitForPendingFinalizers(); } } // diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs index be730c8f21a21..45be39af21656 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs @@ -1,6 +1,7 @@ using System; // using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; @@ -44,6 +45,13 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e) // // + CreateWordDocumentWithCleanup(); + // + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void CreateWordDocumentWithCleanup() + { Word.Application wordApp = null; Word.Document document = null; @@ -66,20 +74,14 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e) if (document != null) { document.Close(true); - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(document); - document = null; + Marshal.FinalReleaseComObject(document); } if (wordApp != null) { wordApp.Quit(true); - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp); - wordApp = null; + Marshal.FinalReleaseComObject(wordApp); } - - GC.Collect(); - GC.WaitForPendingFinalizers(); } - // } // @@ -105,12 +107,38 @@ void DisplayInExcel(IEnumerable accounts, // // - static void CleanupComObject(object comObject) + [MethodImpl(MethodImplOptions.NoInlining)] + static void CreateComObjectsAndCleanup() { - if (comObject != null) + Excel.Application excelApp = null; + Excel.Workbook workbook = null; + Excel.Worksheet worksheet = null; + + try { - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(comObject); - comObject = null; + excelApp = new Excel.Application(); + workbook = excelApp.Workbooks.Add(); + worksheet = workbook.ActiveSheet; + + // Use COM objects here... + } + finally + { + // Clean up COM objects in reverse order of creation + if (worksheet != null) + { + Marshal.FinalReleaseComObject(worksheet); + } + if (workbook != null) + { + workbook.Close(true); + Marshal.FinalReleaseComObject(workbook); + } + if (excelApp != null) + { + excelApp.Quit(); + Marshal.FinalReleaseComObject(excelApp); + } } } // @@ -118,6 +146,13 @@ static void CleanupComObject(object comObject) // void DisplayInExcelWithCleanup(IEnumerable accounts, Action DisplayFunc) + { + DisplayInExcelCore(accounts, DisplayFunc); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + void DisplayInExcelCore(IEnumerable accounts, + Action DisplayFunc) { Excel.Application excelApp = null; Excel.Workbook workbook = null; @@ -157,26 +192,19 @@ void DisplayInExcelWithCleanup(IEnumerable accounts, // Always clean up COM objects in reverse order of creation if (worksheet != null) { - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(worksheet); - worksheet = null; + Marshal.FinalReleaseComObject(worksheet); } if (workbook != null) { workbook.Close(true); // Save changes - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workbook); - workbook = null; + Marshal.FinalReleaseComObject(workbook); } if (excelApp != null) { excelApp.DisplayAlerts = true; excelApp.Quit(); - System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excelApp); - excelApp = null; + Marshal.FinalReleaseComObject(excelApp); } - - // Force garbage collection to help release any remaining references - GC.Collect(); - GC.WaitForPendingFinalizers(); } } // diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index d72424b573758..7eef66a8077d7 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -111,7 +111,7 @@ COM objects in Office Interop require explicit cleanup because: ### Proper cleanup pattern -Use the following pattern to ensure all COM objects are properly released: +The most reliable way to ensure COM objects are properly released is to factor out the COM object creation and usage into a separate non-inlineable method. This pattern guarantees that object references go out of scope and can be collected: :::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="ProperCleanup"::: @@ -122,6 +122,7 @@ Add the following enhanced version of the `DisplayInExcel` method that includes This pattern ensures that: - COM objects are released even if an exception occurs +- Object references are guaranteed to go out of scope when the core method returns - Excel processes don't remain orphaned in Task Manager - Memory is properly freed - The application behaves reliably in production environments @@ -133,8 +134,8 @@ For production applications, always implement this cleanup pattern for every COM **Why can't garbage collection handle this automatically?** COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. -**Why must I set objects to null after calling Marshal.FinalReleaseComObject?** -Setting references to null ensures that your code can't accidentally use a released COM object, which would throw an exception. It also helps the garbage collector by removing any remaining managed references. +**Why do you use separate methods with MethodImpl(MethodImplOptions.NoInlining)?** +The .NET JIT compiler can extend object lifetimes until the end of a method, which means local variable assignments to null aren't guaranteed to release references immediately. By factoring out COM object creation and usage into separate non-inlineable methods, you ensure that object references truly go out of scope when the method returns, allowing reliable cleanup. **Why call GC.Collect() and GC.WaitForPendingFinalizers()?** These calls force immediate garbage collection, which helps ensure that any remaining RCWs are cleaned up promptly. While not always strictly necessary, they provide additional safety in COM interop scenarios. From 94b85e841dfc978fcb37c583b3df133e67ca1d6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:29:24 +0000 Subject: [PATCH 04/10] Update COM cleanup documentation to match code samples - GC calls are optional with reliable method pattern Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../advanced-topics/interop/walkthrough-office-programming.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index 7eef66a8077d7..9605cfbedc408 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -137,8 +137,8 @@ COM objects use reference counting for memory management, which is different fro **Why do you use separate methods with MethodImpl(MethodImplOptions.NoInlining)?** The .NET JIT compiler can extend object lifetimes until the end of a method, which means local variable assignments to null aren't guaranteed to release references immediately. By factoring out COM object creation and usage into separate non-inlineable methods, you ensure that object references truly go out of scope when the method returns, allowing reliable cleanup. -**Why call GC.Collect() and GC.WaitForPendingFinalizers()?** -These calls force immediate garbage collection, which helps ensure that any remaining RCWs are cleaned up promptly. While not always strictly necessary, they provide additional safety in COM interop scenarios. +**Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?** +With the separate non-inlineable method pattern, these calls are typically not necessary. The pattern ensures that COM object references go out of scope reliably when the method returns. However, you can optionally add these calls after the separate method call for additional safety in scenarios where you want to force immediate cleanup. **What happens if I don't follow this pattern?** Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. From 5d03bf1666c836f57fb0fc76b43d191c1eb30982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 03:37:25 +0000 Subject: [PATCH 05/10] Correct COM cleanup documentation based on @jkotas feedback - clarify non-inlinable method pattern is only needed with GC calls Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../interop/snippets/OfficeInterop/program.cs | 18 +++++------------- .../interop/walkthrough-office-programming.md | 5 +---- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index ffba093ff66ba..b3045c2a164a7 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // using Excel = Microsoft.Office.Interop.Excel; @@ -49,12 +48,6 @@ static void Main(string[] args) // static void DisplayInExcel(IEnumerable accounts) - { - DisplayInExcelCore(accounts); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void DisplayInExcelCore(IEnumerable accounts) { Excel.Application excelApp = null; Excel.Workbook workbook = null; @@ -106,17 +99,20 @@ static void DisplayInExcelCore(IEnumerable accounts) if (workSheet != null) { Marshal.FinalReleaseComObject(workSheet); + workSheet = null; } if (workbook != null) { workbook.Close(true); // Save changes Marshal.FinalReleaseComObject(workbook); + workbook = null; } if (excelApp != null) { excelApp.DisplayAlerts = true; excelApp.Quit(); Marshal.FinalReleaseComObject(excelApp); + excelApp = null; } } } @@ -124,12 +120,6 @@ static void DisplayInExcelCore(IEnumerable accounts) // static void CreateIconInWordDoc() - { - CreateIconInWordDocCore(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void CreateIconInWordDocCore() { Word.Application wordApp = null; Word.Document document = null; @@ -164,11 +154,13 @@ static void CreateIconInWordDocCore() { document.Close(true); // Save changes Marshal.FinalReleaseComObject(document); + document = null; } if (wordApp != null) { wordApp.Quit(true); // Save changes to all documents Marshal.FinalReleaseComObject(wordApp); + wordApp = null; } } } diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index 9605cfbedc408..b683e8d3a462e 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -134,11 +134,8 @@ For production applications, always implement this cleanup pattern for every COM **Why can't garbage collection handle this automatically?** COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. -**Why do you use separate methods with MethodImpl(MethodImplOptions.NoInlining)?** -The .NET JIT compiler can extend object lifetimes until the end of a method, which means local variable assignments to null aren't guaranteed to release references immediately. By factoring out COM object creation and usage into separate non-inlineable methods, you ensure that object references truly go out of scope when the method returns, allowing reliable cleanup. - **Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?** -With the separate non-inlineable method pattern, these calls are typically not necessary. The pattern ensures that COM object references go out of scope reliably when the method returns. However, you can optionally add these calls after the separate method call for additional safety in scenarios where you want to force immediate cleanup. +These calls are optional but can help ensure immediate cleanup of Runtime Callable Wrappers (RCWs). The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()`. If you do choose to use `GC.Collect()` and `GC.WaitForPendingFinalizers()`, factor out your COM object usage into a separate method with `MethodImpl(MethodImplOptions.NoInlining)` to ensure object references go out of scope before the GC calls. **What happens if I don't follow this pattern?** Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. From bcfba597e1983e169b2c339ef373e8b780eec390 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 03:41:06 +0000 Subject: [PATCH 06/10] Add missing DisplayAlerts = true line for consistency in COM cleanup pattern Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../interop/snippets/OfficeWalkthrough/ThisAddIn.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs index 45be39af21656..d7dca431c28de 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs @@ -136,6 +136,7 @@ static void CreateComObjectsAndCleanup() } if (excelApp != null) { + excelApp.DisplayAlerts = true; excelApp.Quit(); Marshal.FinalReleaseComObject(excelApp); } From 59e32525891e3ba71152c30a4d1e05be3ea0f3a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 03:51:23 +0000 Subject: [PATCH 07/10] Remove ineffective null assignments and simplify COM cleanup documentation based on @jkotas feedback Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../interop/snippets/OfficeInterop/program.cs | 9 +++------ .../interop/walkthrough-office-programming.md | 16 +++++++--------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index b3045c2a164a7..6faa9d68e463f 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -99,20 +99,19 @@ static void DisplayInExcel(IEnumerable accounts) if (workSheet != null) { Marshal.FinalReleaseComObject(workSheet); - workSheet = null; } if (workbook != null) { workbook.Close(true); // Save changes Marshal.FinalReleaseComObject(workbook); - workbook = null; } if (excelApp != null) { excelApp.DisplayAlerts = true; - excelApp.Quit(); + // Note: Not calling excelApp.Quit() here since this method is + // intended to display data to the user. The Excel instance + // will remain open for the user to interact with. Marshal.FinalReleaseComObject(excelApp); - excelApp = null; } } } @@ -154,13 +153,11 @@ static void CreateIconInWordDoc() { document.Close(true); // Save changes Marshal.FinalReleaseComObject(document); - document = null; } if (wordApp != null) { wordApp.Quit(true); // Save changes to all documents Marshal.FinalReleaseComObject(wordApp); - wordApp = null; } } } diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index b683e8d3a462e..fefe495bea2a8 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -111,31 +111,29 @@ COM objects in Office Interop require explicit cleanup because: ### Proper cleanup pattern -The most reliable way to ensure COM objects are properly released is to factor out the COM object creation and usage into a separate non-inlineable method. This pattern guarantees that object references go out of scope and can be collected: +The essential cleanup pattern is to use try/finally blocks and call `Marshal.FinalReleaseComObject()` on each COM object in reverse order of creation: -:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="ProperCleanup"::: - -Add the following enhanced version of the `DisplayInExcel` method that includes proper COM object cleanup: - -:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="DisplayWithCleanup"::: +:::code language="csharp" source="./snippets/OfficeInterop/program.cs" id="Snippet4"::: This pattern ensures that: - COM objects are released even if an exception occurs -- Object references are guaranteed to go out of scope when the core method returns -- Excel processes don't remain orphaned in Task Manager +- Excel processes don't remain orphaned in Task Manager - Memory is properly freed - The application behaves reliably in production environments For production applications, always implement this cleanup pattern for every COM object you create, including Application, Workbooks, Worksheets, Ranges, and other Office objects. +> [!NOTE] +> The `DisplayInExcel` method shown above does not call `excelApp.Quit()` because it's intended to display data to the user. The Excel instance remains open for user interaction. For automation scenarios where you want to close Excel automatically, add `excelApp.Quit()` before `Marshal.FinalReleaseComObject(excelApp)`. + ### Common questions about COM object cleanup **Why can't garbage collection handle this automatically?** COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. **Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?** -These calls are optional but can help ensure immediate cleanup of Runtime Callable Wrappers (RCWs). The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()`. If you do choose to use `GC.Collect()` and `GC.WaitForPendingFinalizers()`, factor out your COM object usage into a separate method with `MethodImpl(MethodImplOptions.NoInlining)` to ensure object references go out of scope before the GC calls. +These calls are not usually necessary. The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()` when appropriate. Focus on the basic cleanup pattern shown above. **What happens if I don't follow this pattern?** Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. From 54118057bda4a1ecbc1c0be4ea442faef8bbd21b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 03:59:51 +0000 Subject: [PATCH 08/10] Remove unnecessary DisplayAlerts = true lines from COM cleanup examples Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../advanced-topics/interop/snippets/OfficeInterop/program.cs | 1 - .../interop/snippets/OfficeWalkthrough/ThisAddIn.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index 6faa9d68e463f..d66166f9264b5 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -107,7 +107,6 @@ static void DisplayInExcel(IEnumerable accounts) } if (excelApp != null) { - excelApp.DisplayAlerts = true; // Note: Not calling excelApp.Quit() here since this method is // intended to display data to the user. The Excel instance // will remain open for the user to interact with. diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs index d7dca431c28de..909340afc12ff 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs @@ -136,7 +136,6 @@ static void CreateComObjectsAndCleanup() } if (excelApp != null) { - excelApp.DisplayAlerts = true; excelApp.Quit(); Marshal.FinalReleaseComObject(excelApp); } @@ -202,7 +201,6 @@ void DisplayInExcelCore(IEnumerable accounts, } if (excelApp != null) { - excelApp.DisplayAlerts = true; excelApp.Quit(); Marshal.FinalReleaseComObject(excelApp); } From ce5d34f46fcc518269359f975e4732ca9ecf639e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 04:08:46 +0000 Subject: [PATCH 09/10] Fix inaccurate statement about RCW behavior during garbage collection Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../advanced-topics/interop/walkthrough-office-programming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index fefe495bea2a8..776adb3ea3a9e 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -130,7 +130,7 @@ For production applications, always implement this cleanup pattern for every COM ### Common questions about COM object cleanup **Why can't garbage collection handle this automatically?** -COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. +COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object. While the RCW does release the underlying COM object when it's garbage collected, garbage collection in .NET is non-deterministic and can be significantly delayed. **Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?** These calls are not usually necessary. The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()` when appropriate. Focus on the basic cleanup pattern shown above. From c097dc9375b9399dc4fccb3b98fcead1eacf3ded Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 04:44:55 +0000 Subject: [PATCH 10/10] Simplify COM Interop examples based on @jkotas feedback - remove complex cleanup patterns to keep introductory walkthrough simple Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../interop/snippets/OfficeInterop/program.cs | 148 ++++++------------ .../snippets/OfficeWalkthrough/ThisAddIn.cs | 144 +---------------- .../interop/walkthrough-office-programming.md | 42 ----- 3 files changed, 49 insertions(+), 285 deletions(-) diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs index d66166f9264b5..2845cbdbd4ff2 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; // using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; @@ -49,116 +48,59 @@ static void Main(string[] args) // static void DisplayInExcel(IEnumerable accounts) { - Excel.Application excelApp = null; - Excel.Workbook workbook = null; - Excel.Worksheet workSheet = null; - - try - { - excelApp = new Excel.Application(); - // Make the object visible. - excelApp.Visible = true; - - // Create a new, empty workbook and add it to the collection returned - // by property Workbooks. The new workbook becomes the active workbook. - // Add has an optional parameter for specifying a particular template. - // Because no argument is sent in this example, Add creates a new workbook. - workbook = excelApp.Workbooks.Add(); - - // This example uses a single workSheet. The explicit type casting is - // removed in a later procedure. - workSheet = (Excel.Worksheet)excelApp.ActiveSheet; - - // Establish column headings in cells A1 and B1. - workSheet.Cells[1, "A"] = "ID Number"; - workSheet.Cells[1, "B"] = "Current Balance"; - - var row = 1; - foreach (var acct in accounts) - { - row++; - workSheet.Cells[row, "A"] = acct.ID; - workSheet.Cells[row, "B"] = acct.Balance; - } - - workSheet.Columns[1].AutoFit(); - workSheet.Columns[2].AutoFit(); - - // Put the spreadsheet contents on the clipboard. - workSheet.Range["A1:B3"].Copy(); - - // Save the workbook before closing - string fileName = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "BankAccounts.xlsx"); - workbook.SaveAs(fileName); - } - finally + var excelApp = new Excel.Application(); + // Make the object visible. + excelApp.Visible = true; + + // Create a new, empty workbook and add it to the collection returned + // by property Workbooks. The new workbook becomes the active workbook. + // Add has an optional parameter for specifying a particular template. + // Because no argument is sent in this example, Add creates a new workbook. + excelApp.Workbooks.Add(); + + // This example uses a single workSheet. The explicit type casting is + // removed in a later procedure. + Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet; + + // Establish column headings in cells A1 and B1. + workSheet.Cells[1, "A"] = "ID Number"; + workSheet.Cells[1, "B"] = "Current Balance"; + + var row = 1; + foreach (var acct in accounts) { - // Clean up COM objects in reverse order of creation - if (workSheet != null) - { - Marshal.FinalReleaseComObject(workSheet); - } - if (workbook != null) - { - workbook.Close(true); // Save changes - Marshal.FinalReleaseComObject(workbook); - } - if (excelApp != null) - { - // Note: Not calling excelApp.Quit() here since this method is - // intended to display data to the user. The Excel instance - // will remain open for the user to interact with. - Marshal.FinalReleaseComObject(excelApp); - } + row++; + workSheet.Cells[row, "A"] = acct.ID; + workSheet.Cells[row, "B"] = acct.Balance; } + + workSheet.Columns[1].AutoFit(); + workSheet.Columns[2].AutoFit(); + + // Put the spreadsheet contents on the clipboard. The Copy method has one + // optional parameter for specifying a destination. Because no argument + // is sent, the destination is the Clipboard. + workSheet.Range["A1:B3"].Copy(); } // // static void CreateIconInWordDoc() { - Word.Application wordApp = null; - Word.Document document = null; - - try - { - wordApp = new Word.Application(); - wordApp.Visible = true; - - // The Add method has four reference parameters, all of which are - // optional. Visual C# allows you to omit arguments for them if - // the default values are what you want. - document = wordApp.Documents.Add(); - - // PasteSpecial has seven reference parameters, all of which are - // optional. This example uses named arguments to specify values - // for two of the parameters. Although these are reference - // parameters, you do not need to use the ref keyword, or to create - // variables to send in as arguments. You can send the values directly. - wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); - - // Save the document - string fileName = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "BankAccountsLink.docx"); - document.SaveAs(fileName); - } - finally - { - // Clean up COM objects in reverse order of creation - if (document != null) - { - document.Close(true); // Save changes - Marshal.FinalReleaseComObject(document); - } - if (wordApp != null) - { - wordApp.Quit(true); // Save changes to all documents - Marshal.FinalReleaseComObject(wordApp); - } - } + var wordApp = new Word.Application(); + wordApp.Visible = true; + + // The Add method has four reference parameters, all of which are + // optional. Visual C# allows you to omit arguments for them if + // the default values are what you want. + wordApp.Documents.Add(); + + // PasteSpecial has seven reference parameters, all of which are + // optional. This example uses named arguments to specify values + // for two of the parameters. Although these are reference + // parameters, you do not need to use the ref keyword, or to create + // variables to send in as arguments. You can send the values directly. + wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); } // diff --git a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs index 909340afc12ff..fc559caf33d22 100644 --- a/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs +++ b/docs/csharp/advanced-topics/interop/snippets/OfficeWalkthrough/ThisAddIn.cs @@ -1,8 +1,6 @@ using System; // using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; // @@ -45,45 +43,13 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e) // // - CreateWordDocumentWithCleanup(); + var wordApp = new Word.Application(); + wordApp.Visible = true; + wordApp.Documents.Add(); + wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true); // } - [MethodImpl(MethodImplOptions.NoInlining)] - private void CreateWordDocumentWithCleanup() - { - Word.Application wordApp = null; - Word.Document document = null; - - try - { - wordApp = new Word.Application(); - wordApp.Visible = true; - document = wordApp.Documents.Add(); - wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true); - - // Save the document - string fileName = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "BankAccountsLink.docx"); - document.SaveAs(fileName); - } - finally - { - // Clean up COM objects - if (document != null) - { - document.Close(true); - Marshal.FinalReleaseComObject(document); - } - if (wordApp != null) - { - wordApp.Quit(true); - Marshal.FinalReleaseComObject(wordApp); - } - } - } - // void DisplayInExcel(IEnumerable accounts, Action DisplayFunc) @@ -106,108 +72,6 @@ void DisplayInExcel(IEnumerable accounts, } // - // - [MethodImpl(MethodImplOptions.NoInlining)] - static void CreateComObjectsAndCleanup() - { - Excel.Application excelApp = null; - Excel.Workbook workbook = null; - Excel.Worksheet worksheet = null; - - try - { - excelApp = new Excel.Application(); - workbook = excelApp.Workbooks.Add(); - worksheet = workbook.ActiveSheet; - - // Use COM objects here... - } - finally - { - // Clean up COM objects in reverse order of creation - if (worksheet != null) - { - Marshal.FinalReleaseComObject(worksheet); - } - if (workbook != null) - { - workbook.Close(true); - Marshal.FinalReleaseComObject(workbook); - } - if (excelApp != null) - { - excelApp.Quit(); - Marshal.FinalReleaseComObject(excelApp); - } - } - } - // - - // - void DisplayInExcelWithCleanup(IEnumerable accounts, - Action DisplayFunc) - { - DisplayInExcelCore(accounts, DisplayFunc); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - void DisplayInExcelCore(IEnumerable accounts, - Action DisplayFunc) - { - Excel.Application excelApp = null; - Excel.Workbook workbook = null; - Excel.Worksheet worksheet = null; - - try - { - excelApp = new Excel.Application(); - excelApp.Visible = true; - - // Add a new Excel workbook. - workbook = excelApp.Workbooks.Add(); - worksheet = workbook.ActiveSheet; - - worksheet.Cells[1, 1].Value = "ID"; - worksheet.Cells[1, 2].Value = "Balance"; - - int row = 2; - foreach (var ac in accounts) - { - var cell = worksheet.Cells[row, 1]; - DisplayFunc(ac, cell); - row++; - } - - // Save the workbook - string fileName = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "BankAccounts.xlsx"); - workbook.SaveAs(fileName); - - // Copy the results to the Clipboard. - worksheet.Range["A1:B3"].Copy(); - } - finally - { - // Always clean up COM objects in reverse order of creation - if (worksheet != null) - { - Marshal.FinalReleaseComObject(worksheet); - } - if (workbook != null) - { - workbook.Close(true); // Save changes - Marshal.FinalReleaseComObject(workbook); - } - if (excelApp != null) - { - excelApp.Quit(); - Marshal.FinalReleaseComObject(excelApp); - } - } - } - // - private void ThisAddIn_Shutdown(object sender, System.EventArgs e) { diff --git a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md index 776adb3ea3a9e..f24beeb243e6d 100644 --- a/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md +++ b/docs/csharp/advanced-topics/interop/walkthrough-office-programming.md @@ -96,48 +96,6 @@ This code demonstrates several of the features in C#: the ability to omit the `r Press F5 to run the application. Excel starts and displays a table that contains the information from the two accounts in `bankAccounts`. Then a Word document appears that contains a link to the Excel table. -## Important: COM object cleanup and resource management - -The examples shown above demonstrate basic Office Interop functionality, but they don't include proper cleanup of COM objects. This is a critical issue in production applications because failing to properly release COM objects can result in orphaned Office processes that remain in memory even after your application closes. - -### Why COM object cleanup is necessary - -COM objects in Office Interop require explicit cleanup because: - -- The .NET garbage collector doesn't automatically release COM objects -- Each Excel or Word object you create holds resources that must be manually released -- Without proper cleanup, Office applications remain running in the background -- This applies to all COM objects: Application, Workbooks, Worksheets, Ranges, and more - -### Proper cleanup pattern - -The essential cleanup pattern is to use try/finally blocks and call `Marshal.FinalReleaseComObject()` on each COM object in reverse order of creation: - -:::code language="csharp" source="./snippets/OfficeInterop/program.cs" id="Snippet4"::: - -This pattern ensures that: - -- COM objects are released even if an exception occurs -- Excel processes don't remain orphaned in Task Manager -- Memory is properly freed -- The application behaves reliably in production environments - -For production applications, always implement this cleanup pattern for every COM object you create, including Application, Workbooks, Worksheets, Ranges, and other Office objects. - -> [!NOTE] -> The `DisplayInExcel` method shown above does not call `excelApp.Quit()` because it's intended to display data to the user. The Excel instance remains open for user interaction. For automation scenarios where you want to close Excel automatically, add `excelApp.Quit()` before `Marshal.FinalReleaseComObject(excelApp)`. - -### Common questions about COM object cleanup - -**Why can't garbage collection handle this automatically?** -COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object. While the RCW does release the underlying COM object when it's garbage collected, garbage collection in .NET is non-deterministic and can be significantly delayed. - -**Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?** -These calls are not usually necessary. The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()` when appropriate. Focus on the basic cleanup pattern shown above. - -**What happens if I don't follow this pattern?** -Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. - ## Clean up the completed project In Visual Studio, select **Clean Solution** on the **Build** menu. Otherwise, the add-in runs every time that you open Excel on your computer.