How do I properly clean up Excel interop objects?

Viewed 337876

I'm using the Excel interop in C# (ApplicationClass) and have placed the following code in my finally clause:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

Although this kind of works, the Excel.exe process is still in the background even after I close Excel. It is only released once my application is manually closed.

What am I doing wrong, or is there an alternative to ensure interop objects are properly disposed of?

43 Answers

Excel does not quit because your application is still holding references to COM objects.

I guess you're invoking at least one member of a COM object without assigning it to a variable.

For me it was the excelApp.Worksheets object which I directly used without assigning it to a variable:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

I didn't know that internally C# created a wrapper for the Worksheets COM object which didn't get released by my code (because I wasn't aware of it) and was the cause why Excel was not unloaded.

I found the solution to my problem on this page, which also has a nice rule for the usage of COM objects in C#:

Never use two dots with COM objects.


So with this knowledge the right way of doing the above is:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

POST MORTEM UPDATE:

I want every reader to read this answer by Hans Passant very carefully as it explains the trap I and lots of other developers stumbled into. When I wrote this answer years ago I didn't know about the effect the debugger has to the garbage collector and drew the wrong conclusions. I keep my answer unaltered for the sake of history but please read this link and don't go the way of "the two dots": Understanding garbage collection in .NET and Clean up Excel Interop Objects with IDisposable

You can actually release your Excel Application object cleanly, but you do have to take care.

The advice to maintain a named reference for absolutely every COM object you access and then explicitly release it via Marshal.FinalReleaseComObject() is correct in theory, but, unfortunately, very difficult to manage in practice. If one ever slips anywhere and uses "two dots", or iterates cells via a for each loop, or any other similar kind of command, then you'll have unreferenced COM objects and risk a hang. In this case, there would be no way to find the cause in the code; you would have to review all your code by eye and hopefully find the cause, a task that could be nearly impossible for a large project.

The good news is that you do not actually have to maintain a named variable reference to every COM object you use. Instead, call GC.Collect() and then GC.WaitForPendingFinalizers() to release all the (usually minor) objects to which you do not hold a reference, and then explicitly release the objects to which you do hold a named variable reference.

You should also release your named references in reverse order of importance: range objects first, then worksheets, workbooks, and then finally your Excel Application object.

For example, assuming that you had a Range object variable named xlRng, a Worksheet variable named xlSheet, a Workbook variable named xlBook and an Excel Application variable named xlApp, then your cleanup code could look something like the following:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

In most code examples you'll see for cleaning up COM objects from .NET, the GC.Collect() and GC.WaitForPendingFinalizers() calls are made TWICE as in:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

This should not be required, however, unless you are using Visual Studio Tools for Office (VSTO), which uses finalizers that cause an entire graph of objects to be promoted in the finalization queue. Such objects would not be released until the next garbage collection. However, if you are not using VSTO, you should be able to call GC.Collect() and GC.WaitForPendingFinalizers() just once.

I know that explicitly calling GC.Collect() is a no-no (and certainly doing it twice sounds very painful), but there is no way around it, to be honest. Through normal operations you will generate hidden objects to which you hold no reference that you, therefore, cannot release through any other means other than calling GC.Collect().

This is a complex topic, but this really is all there is to it. Once you establish this template for your cleanup procedure you can code normally, without the need for wrappers, etc. :-)

I have a tutorial on this here:

Automating Office Programs with VB.Net / COM Interop

It's written for VB.NET, but don't be put off by that, the principles are exactly the same as when using C#.

Preface: my answer contains two solutions, so be careful when reading and don't miss anything.

There are different ways and advice of how to make Excel instance unload, such as:

  • Releasing EVERY com object explicitly with Marshal.FinalReleaseComObject() (not forgetting about implicitly created com-objects). To release every created com object, you may use the rule of 2 dots mentioned here:
    How do I properly clean up Excel interop objects?

  • Calling GC.Collect() and GC.WaitForPendingFinalizers() to make CLR release unused com-objects * (Actually, it works, see my second solution for details)

  • Checking if com-server-application maybe shows a message box waiting for the user to answer (though I am not sure it can prevent Excel from closing, but I heard about it a few times)

  • Sending WM_CLOSE message to the main Excel window

  • Executing the function that works with Excel in a separate AppDomain. Some people believe Excel instance will be shut, when AppDomain is unloaded.

  • Killing all excel instances which were instantiated after our excel-interoping code started.

BUT! Sometimes all these options just don't help or can't be appropriate!

For example, yesterday I found out that in one of my functions (which works with excel) Excel keeps running after the function ends. I tried everything! I thoroughly checked the whole function 10 times and added Marshal.FinalReleaseComObject() for everything! I also had GC.Collect() and GC.WaitForPendingFinalizers(). I checked for hidden message boxes. I tried to send WM_CLOSE message to the main Excel window. I executed my function in a separate AppDomain and unloaded that domain. Nothing helped! The option with closing all excel instances is inappropriate, because if the user starts another Excel instance manually, during execution of my function which works also with Excel, then that instance will also be closed by my function. I bet the user will not be happy! So, honestly, this is a lame option (no offence guys). So I spent a couple of hours before I found a good (in my humble opinion) solution: Kill excel process by hWnd of its main window (it's the first solution).

Here is the simple code:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

As you can see I provided two methods, according to Try-Parse pattern (I think it is appropriate here): one method doesn't throw the exception if the Process could not be killed (for example the process doesn't exist anymore), and another method throws the exception if the Process was not killed. The only weak place in this code is security permissions. Theoretically, the user may not have permissions to kill the process, but in 99.99% of all cases, user has such permissions. I also tested it with a guest account - it works perfectly.

So, your code, working with Excel, can look like this:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

Voila! Excel is terminated! :)

Ok, let's go back to the second solution, as I promised in the beginning of the post. The second solution is to call GC.Collect() and GC.WaitForPendingFinalizers(). Yes, they actually work, but you need to be careful here!
Many people say (and I said) that calling GC.Collect() doesn't help. But the reason it wouldn't help is if there are still references to COM objects! One of the most popular reasons for GC.Collect() not being helpful is running the project in Debug-mode. In debug-mode objects that are not really referenced anymore will not be garbage collected until the end of the method.
So, if you tried GC.Collect() and GC.WaitForPendingFinalizers() and it didn't help, try to do the following:

1) Try to run your project in Release mode and check if Excel closed correctly

2) Wrap the method of working with Excel in a separate method. So, instead of something like this:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

you write:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

Now, Excel will close =)

This worked for a project I was working on:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

We learned that it was important to set every reference to an Excel COM object to null when you were done with it. This included Cells, Sheets, and everything.

Anything that is in the Excel namespace needs to be released. Period

You can't be doing:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

You have to be doing

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

followed by the releasing of the objects.

I found a useful generic template that can help implement the correct disposal pattern for COM objects, that need Marshal.ReleaseComObject called when they go out of scope:

Usage:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

Template:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

Reference:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

The accepted answer here is correct, but also take note that not only "two dot" references need to be avoided, but also objects that are retrieved via the index. You also do not need to wait until you are finished with the program to clean up these objects, it's best to create functions that will clean them up as soon as you're finished with them, when possible. Here is a function I created that assigns some properties of a Style object called xlStyleHeader:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

Notice that I had to set xlBorders[Excel.XlBordersIndex.xlEdgeBottom] to a variable in order to clean that up (Not because of the two dots, which refer to an enumeration which does not need to be released, but because the object I'm referring to is actually a Border object that does need to be released).

This sort of thing is not really necessary in standard applications, which do a great job of cleaning up after themselves, but in ASP.NET applications, if you miss even one of these, no matter how often you call the garbage collector, Excel will still be running on your server.

It requires a lot of attention to detail and many test executions while monitoring the Task Manager when writing this code, but doing so saves you the hassle of desperately searching through pages of code to find the one instance you missed. This is especially important when working in loops, where you need to release EACH INSTANCE of an object, even though it uses the same variable name each time it loops.

After trying

  1. Release COM objects in reverse order
  2. Add GC.Collect() and GC.WaitForPendingFinalizers() twice at the end
  3. No more than two dots
  4. Close workbook and quit application
  5. Run in release mode

the final solution that works for me is to move one set of

GC.Collect();
GC.WaitForPendingFinalizers();

that we added to the end of the function to a wrapper, as follows:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

You need to be aware that Excel is very sensitive to the culture you are running under as well.

You may find that you need to set the culture to EN-US before calling Excel functions. This does not apply to all functions - but some of them.

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

This applies even if you are using VSTO.

For details: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369

As others have pointed out, you need to create an explicit reference for every Excel object you use, and call Marshal.ReleaseComObject on that reference, as described in this KB article. You also need to use try/finally to ensure ReleaseComObject is always called, even when an exception is thrown. I.e. instead of:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

you need to do something like:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

You also need to call Application.Quit before releasing the Application object if you want Excel to close.

As you can see, this quickly becomes extremely unwieldy as soon as you try to do anything even moderately complex. I have successfully developed .NET applications with a simple wrapper class that wraps a few simple manipulations of the Excel object model (open a workbook, write to a Range, save/close the workbook etc). The wrapper class implements IDisposable, carefully implements Marshal.ReleaseComObject on every object it uses, and does not pubicly expose any Excel objects to the rest of the app.

But this approach doesn't scale well for more complex requirements.

This is a big deficiency of .NET COM Interop. For more complex scenarios, I would seriously consider writing an ActiveX DLL in VB6 or other unmanaged language to which you can delegate all interaction with out-proc COM objects such as Office. You can then reference this ActiveX DLL from your .NET application, and things will be much easier as you will only need to release this one reference.

You should be very careful using Word/Excel interop applications. After trying all the solutions we still had a lot of "WinWord" process left open on server (with more than 2000 users).

After working on the problem for hours, I realized that if I open more than a couple of documents using Word.ApplicationClass.Document.Open() on different threads simultaneously, IIS worker process (w3wp.exe) would crash leaving all WinWord processes open!

So I guess there is no absolute solution to this problem, but switching to other methods such as Office Open XML development.

I think that some of that is just the way that the framework handles Office applications, but I could be wrong. On some days, some applications clean up the processes immediately, and other days it seems to wait until the application closes. In general, I quit paying attention to the details and just make sure that there aren't any extra processes floating around at the end of the day.

Also, and maybe I'm over simplifying things, but I think you can just...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

Like I said earlier, I don't tend to pay attention to the details of when the Excel process appears or disappears, but that usually works for me. I also don't like to keep Excel processes around for anything other than the minimal amount of time, but I'm probably just being paranoid on that.

Here is a really easy way to do it:

[DllImport("User32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
...

int objExcelProcessId = 0;

Excel.Application objExcel = new Excel.Application();

GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);

Process.GetProcessById(objExcelProcessId).Kill();

My answer is late and its only purpose is to support the solution proposed by Govert.

Short version:

  • Write a local function with no global variables and no arguments executing the COM stuff.

  • Call the COM function in a wrapping function that calls the COM function and cleans thereafter.

Long version:

You are not using .Net to count references of COM objects and to release them yourself in the correct order. Even C++ programmers don't do that any longer by using smart pointers. So, forget about Marshal.ReleaseComObject and the funny one dot good two dots bad rule. The GC is happy to do the chore of releasing COM objects if you null out all references to COM objects that are no longer needed. The easiest way is to handle COM objects in a local function, with all variables for COM objects naturally going out of scope at the end. Due to some strange features of the debugger pointed out in the brilliant answers of Hans Passant mentioned in the accepted answers Post Mortem, the cleanup should be delegated to a wrapping function that also calls the executing function. So, COM objects like Excel or Word need two functions, one that does the actual job and a wrapper that calls this function and calls the GC afterwards like Govert did, the only correct answer in this thread. To show the principle I use a wrapper suitable for all functions doing COM stuff. Except for this extension, my code is just the C# version of Govert's code. In addition, I stopped the process for 6 seconds so that you can check out in the Task Manager that Excel is no longer visible after Quit() but lives on as a zombie until the GC puts an end to it.

using Excel = Microsoft.Office.Interop.Excel;
public delegate void WrapCom();
namespace GCTestOnOffice{
  class Program{
    static void DoSomethingWithExcel(){
      Excel.Application ExcelApp = new();
      Excel.Workbook Wb = ExcelApp.Workbooks.Open(@"D:\\Sample.xlsx");
      Excel.Worksheet NewWs = Wb.Worksheets.Add();
      for (int i = 1; i < 10; i++){ NewWs.Cells[i, 1] = i;}
      Wb.Save();
      ExcelApp.Quit();
    } 

    static void TheComWrapper(WrapCom wrapCom){
      wrapCom();
      //All COM objects are out of scope, ready for the GC to gobble
      //Excel is no longer visible, but the process is still alive,
      //check out the Task-Manager in the next 6 seconds
      Thread.Sleep(6000);
      GC.Collect();
      GC.WaitForPendingFinalizers();
      GC.Collect();
      GC.WaitForPendingFinalizers();
      //Check out the Task-Manager, the Excel process is gone
    }

    static void Main(string[] args){
      TheComWrapper(DoSomethingWithExcel);
    }
  }
}

There i have an idea,try to kill the excel process you have opened:

  1. before open an excelapplication,get all the process ids named oldProcessIds.
  2. open the excelapplication.
  3. get now all the excelapplication process ids named nowProcessIds.
  4. when need to quit,kill the except ids between oldProcessIds and nowProcessIds.

    private static Excel.Application GetExcelApp()
         {
            if (_excelApp == null)
            {
                var processIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApp = new Excel.Application();
                _excelApp.DisplayAlerts = false;
    
                _excelApp.Visible = false;
                _excelApp.ScreenUpdating = false;
                var newProcessIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApplicationProcessId = newProcessIds.Except(processIds).FirstOrDefault();
            }
    
            return _excelApp;
        }
    
    public static void Dispose()
        {
            try
            {
                _excelApp.Workbooks.Close();
                _excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp);
                _excelApp = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                if (_excelApplicationProcessId != default(int))
                {
                    var process = System.Diagnostics.Process.GetProcessById(_excelApplicationProcessId);
                    process?.Kill();
                    _excelApplicationProcessId = default(int);
                }
            }
            catch (Exception ex)
            {
                _excelApp = null;
            }
    
        }
    

I had this same problem getting PowerPoint to close after newing up the Application object in my VSTO AddIn. I tried all the answers here with limited success.

This is the solution I found for my case - DONT use 'new Application', the AddInBase base class of ThisAddIn already has a handle to 'Application'. If you use that handle where you need it (make it static if you have to) then you don't need to worry about cleaning it up and PowerPoint won't hang on close.

Of the three general strategies considered in other answers, killing the excel process is clearly a hack, whereas invoking the garbage collector is a brutal shotgun approach meant to compensate for incorrect deallocation of COM-objects. After lots of experimentation and rewriting the management of COM objects in my version-agnostic and late-bound wrapper, I have come to the conclusion that accurate and timely invocations of Marshal.ReleaseComObject() is the most efficient and elegant strategy. And no, you do not ever need FinalReleaseComObject(), because in a well-writtin program each COM acquired on once and therefore requires a single decrement of the reference counter.

One shall make sure to release every single COM object, preferably as soon as it is no longer needed. But it is perfectly possible to release everything right after quitting the Excel application, at the only expense of higher memory usage. Excel will close as expected as long as one does not loose or forget to release a COM object.

The simplest and most obvious aid in the process is wrapping every interop object into a .NET class implementing IDisposable, where the Dispose() method invokes ReleaseComObject() on its interop object. Doing it in the destructor, as proposed in here, makes no sense because destructors are non-deterministic.

Show below is our wrapper's method that obtains a cell from WorkSheet bypassing the intermediate Cells member. Notice the way it disposes of the intermediate object after use:

public ExcelRange XCell( int row, int col)
{   ExcelRange anchor, res;
    using( anchor = Range( "A1") )
    {   res = anchor.Offset( row - 1, col - 1 );  }
    return res;
}

The next step may be a simple memory manager that will keep track of every COM object obtained and make sure to release it after Excel quits if the user prefers to trade some RAM usage for simpler code.

Futher reading

  1. How to properly release Excel COM objects,
  2. Releasing COM objects: Garbage Collector vs. Marshal.RelseaseComObject.

I really like when things clean up after them selves... So I made some wrapper classes that do all the cleanup for me! These are documented further down.

The end code is quite readable and accessible. I haven't yet found any phantom instances of Excel running after I Close() the workbooks and Quit() the application (besides where I debug and close the app mid process).

function void OpenCopyClose() {
  var excel = new ExcelApplication();
  var workbook1 = excel.OpenWorkbook("C:\Temp\file1.xslx", readOnly: true);
  var readOnlysheet = workbook1.Worksheet("sheet1");

  var workbook2 = excel.OpenWorkbook("C:\Temp\file2.xslx");
  var writeSheet = workbook.Worksheet("sheet1");

  // do all the excel manipulation

  // read from the first workbook, write to the second workbook.
  var a1 = workbook1.Cells[1, 1];
  workbook2.Cells[1, 1] = a1

  // explicit clean-up
  workbook1.Close(false);
  workbook2 .Close(true);
  excel.Quit();
}

Note: You can skip the Close() and Quit() calls but if you are writing to an Excel document you will at least want to Save(). When the objects go out of scope (the method returns) the class finalizers will automatically kick in and do any cleanup. Any references to COM objects from the Worksheet COM object will automatically be managed and cleaned up as long as you are careful with the scope of your variables, eg keep variables local to the current scope only when storing references to COM objects. You can easily copy values you need to POCOs if you need, or create additional wrapper classes as discussed below.

To manage all this, I have created a class, DisposableComObject, that acts as a wrapper for any COM object. It implements the IDisposable interface and also contains a finalizer for those that don't like using.

The Dispose() method calls Marshal.ReleaseComObject(ComObject) and then sets the ComObjectRef property to null.

The object is in a disposed state when the private ComObjectRef property is null.

If the ComObject property is accessed after being disposed, a ComObjectAccessedAfterDisposeException exception is thrown.

The Dispose() method can be called manually. It is also called by the finalizer, at the conclusion of a using block, and for using var at the conclusion of the scope of that variable.

The top level classes from Microsoft.Office.Interop.Excel, Application, Workbook, and Worksheet, get their own wrapper classes where each are subclasses of DisposableComObject

Here is the code:

/// <summary>
/// References to COM objects must be explicitly released when done.
/// Failure to do so can result in odd behavior and processes remaining running after the application has stopped.
/// This class helps to automate the process of disposing the references to COM objects.
/// </summary>
public abstract class DisposableComObject : IDisposable
{
    public class ComObjectAccessedAfterDisposeException : Exception
    {
        public ComObjectAccessedAfterDisposeException() : base("COM object has been accessed after being disposed") { }
    }

    /// <summary>The actual COM object</summary>
    private object ComObjectRef { get; set; }

    /// <summary>The COM object to be used by subclasses</summary>
    /// <exception cref="ComObjectAccessedAfterDisposeException">When the COM object has been disposed</exception>
    protected object ComObject => ComObjectRef ?? throw new ComObjectAccessedAfterDisposeException();

    public DisposableComObject(object comObject) => ComObjectRef = comObject;

    /// <summary>
    /// True, if the COM object has been disposed.
    /// </summary>
    protected bool IsDisposed() => ComObjectRef is null;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // in case a subclass implements a finalizer
    }

    /// <summary>
    /// This method releases the COM object and removes the reference.
    /// This allows the garbage collector to clean up any remaining instance.
    /// </summary>
    /// <param name="disposing">Set to true</param>
    protected virtual void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        Marshal.ReleaseComObject(ComObject);
        ComObjectRef = null;
    }

    ~DisposableComObject()
    {
        Dispose(true);
    }
}

There is also a handy generic subclass which makes usage slightly easier.

public abstract class DisposableComObject<T> : DisposableComObject
{
    protected new T ComObject => (T)base.ComObject;

    public DisposableComObject(T comObject) : base(comObject) { }
}

Finally, we can use DisposableComObject<T> to create our wrapper classes for the Excel interop classes.

The ExcelApplication subclass has a reference to a new Excel application instance and is used to open workbooks.

OpenWorkbook() returns an ExcelWorkbook which is also a subclass of DisposableComObject.

Dispose() has been overridden to quit the Excel application before calling the base Dispose() method. Quit() is an alias of Dispose().

public class ExcelApplication : DisposableComObject<Application>
{
    public class OpenWorkbookActionCancelledException : Exception
    {
        public string Filename { get; }

        public OpenWorkbookActionCancelledException(string filename, COMException ex) : base($"The workbook open action was cancelled. {ex.Message}", ex) => Filename = filename;
    }

    /// <summary>The actual Application from Interop.Excel</summary>
    Application App => ComObject;

    public ExcelApplication() : base(new Application()) { }

    /// <summary>Open a workbook.</summary>
    public ExcelWorkbook OpenWorkbook(string filename, bool readOnly = false, string password = null, string writeResPassword = null)
    {
        try
        {
            var workbook = App.Workbooks.Open(Filename: filename, UpdateLinks: (XlUpdateLinks)0, ReadOnly: readOnly, Password: password, WriteResPassword: writeResPassword, );

            return new ExcelWorkbook(workbook);
        }
        catch (COMException ex)
        {
            // If the workbook is already open and the request mode is not read-only, the user will be presented
            // with a prompt from the Excel application asking if the workbook should be opened in read-only mode.
            // This exception is raised when when the user clicks the Cancel button in that prompt.
            throw new OpenWorkbookActionCancelledException(filename, ex);
        }
    }

    /// <summary>Quit the running application.</summary>
    public void Quit() => Dispose(true);

    /// <inheritdoc/>
    protected override void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        App.Quit();
        base.Dispose(disposing);
    }
}

ExcelWorkbook also subclasses DisposableComObject<Workbook> and is used to open worksheets.

The Worksheet() methods returns ExcelWorksheet which, you guessed it, is also an subclass of DisposableComObject<Workbook>.

The Dispose() method is overridden and fist closes the worksheet before calling the base Dispose().

NOTE: I've added some extension methods which is uses to iterate over Workbook.Worksheets. If you get compile errors, this is why. Ill add the extension methods at the end.

public class ExcelWorkbook : DisposableComObject<Workbook>
{
    public class WorksheetNotFoundException : Exception
    {
        public WorksheetNotFoundException(string message) : base(message) { }
    }

    /// <summary>The actual Workbook from Interop.Excel</summary>
    Workbook Workbook => ComObject;

    /// <summary>The worksheets within the workbook</summary>
    public IEnumerable<ExcelWorksheet> Worksheets => worksheets ?? (worksheets = Workbook.Worksheets.AsEnumerable<Worksheet>().Select(w => new ExcelWorksheet(w)).ToList());
    private IEnumerable<ExcelWorksheet> worksheets;

    public ExcelWorkbook(Workbook workbook) : base(workbook) { }

    /// <summary>
    /// Get the worksheet matching the <paramref name="sheetName"/>
    /// </summary>
    /// <param name="sheetName">The name of the Worksheet</param>
    public ExcelWorksheet Worksheet(string sheetName) => Worksheet(s => s.Name == sheetName, () => $"Worksheet not found: {sheetName}");

    /// <summary>
    /// Get the worksheet matching the <paramref name="predicate"/>
    /// </summary>
    /// <param name="predicate">A function to test each Worksheet for a macth</param>
    public ExcelWorksheet Worksheet(Func<ExcelWorksheet, bool> predicate, Func<string> errorMessageAction) => Worksheets.FirstOrDefault(predicate) ??  throw new WorksheetNotFoundException(errorMessageAction.Invoke());

    /// <summary>
    /// Returns true of the workbook is read-only
    /// </summary>
    public bool IsReadOnly() => Workbook.ReadOnly;

    /// <summary>
    /// Save changes made to the workbook
    /// </summary>
    public void Save()
    {
        Workbook.Save();
    }

    /// <summary>
    /// Close the workbook and optionally save changes
    /// </summary>
    /// <param name="saveChanges">True is save before close</param>
    public void Close(bool saveChanges)
    {
        if (saveChanges) Save();
        Dispose(true);
    }

    /// <inheritdoc/>
    protected override void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        Workbook.Close();
        base.Dispose(disposing);
    }
}

Finally, the ExcelWorksheet.

UsedRows() simply returns an enumerable of unwrapped Microsoft.Office.Interop.Excel.Range objects. I haven't yet encountered a situation where COM objects accessed from properties of the Microsoft.Office.Interop.Excel.Worksheet object need to manually wrapped like was needed with Application, Workbook, and Worksheet. These all seem to clean them selves up automatically. Mostly, I was just iterating over Ranges and getting or setting values, so my particular use-case isn't as advanced as the available functionality.

There is no override of Dispose() in this case as no special action needs to take place for worksheets.

public class ExcelWorksheet : DisposableComObject<Worksheet>
{
    /// <summary>The actual Worksheet from Interop.Excel</summary>
    Worksheet Worksheet => ComObject;

    /// <summary>The worksheet name</summary>
    public string Name => Worksheet.Name;

    // <summary>The worksheets cells (Unwrapped COM object)</summary>
    public Range Cells => Worksheet.Cells;

    public ExcelWorksheet(Worksheet worksheet) : base(worksheet) { }

    /// <inheritdoc cref="WorksheetExtensions.UsedRows(Worksheet)"/>
    public IEnumerable<Range> UsedRows() => Worksheet.UsedRows().ToList();
}

It is possible to add even more wrapper classes. Just add additional methods to ExcelWorksheet as needed and return the COM object in a wrapper class. Just copy what we did when wrapping the workbook via ExcelApplication.OpenWorkbook() and ExcelWorkbook.WorkSheets.

Some useful extension methods:

public static class EnumeratorExtensions
{
    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerable enumerator)
    {
        return enumerator.GetEnumerator().AsEnumerable<T>();
    }

    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerator enumerator)
    {
        while (enumerator.MoveNext()) yield return (T)enumerator.Current;
    }

    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerator<T> enumerator)
    {
        while (enumerator.MoveNext()) yield return enumerator.Current;
    }
}

public static class WorksheetExtensions
{
    /// <summary>
    /// Returns the rows within the used range of this <paramref name="worksheet"/>
    /// </summary>
    /// <param name="worksheet">The worksheet</param>
    public static IEnumerable<Range> UsedRows(this Worksheet worksheet) =>
        worksheet.UsedRange.Rows.AsEnumerable<Range>();
}
Related