1
votes

I need to get EntryIDs for all contacts in Outlook folder. If I have 6000 contacts in a folder, it takes about 100 seconds to execute (it doesn't matter much if if's background thread or main thread, I tried both). The code is like this:

List<Outlook.ContactItem> contactItemsList = null;
Outlook.Items folderItems = null;
Outlook.MAPIFolder folderSuggestedContacts = null;
Outlook.NameSpace ns = null;
Outlook.MAPIFolder folderContacts = null;
object itemObj = null;
try
{
    contactItemsList = new List<Outlook.ContactItem>();
    ns = Application.GetNamespace("MAPI");
    // getting items from the Contacts folder in Outlook
    folderContacts = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
    folderItems = folderContacts.Items;
    for (int i = 1; folderItems.Count >= i; i++)
    {
        itemObj = folderItems[i];
        if (itemObj is Outlook.ContactItem)
            contactItemsList.Add(itemObj as Outlook.ContactItem);
        else
            Marshal.ReleaseComObject(itemObj);
    }
    Marshal.ReleaseComObject(folderItems);
    folderItems = null;
}
catch (Exception ex)
{
    System.Windows.Forms.MessageBox.Show(ex.Message);
}
finally
{
    if (folderItems != null)
        Marshal.ReleaseComObject(folderItems);
    if (folderContacts != null)
        Marshal.ReleaseComObject(folderContacts);
    if (folderSuggestedContacts != null)
        Marshal.ReleaseComObject(folderSuggestedContacts);
    if (ns != null)
        Marshal.ReleaseComObject(ns);
}
var ids = contactItemsList.Select(c => c.EntryID).ToArray();

The part to collect items takes about 5-8 seconds while the last line takes about 80-90 seconds.

Is there a faster way? I thought of Items.SetColumns initially but it turns out it doesn't work for EntryID (which seems strange decision of Outlook developers as EntryID is just a string and SetColumns is intended for quick retrieval of string properties).

Moreover, Outlook almost freezes for all this time (~100 seconds) even with BackgroundWorker. You can accidentally click something but it's like FPS rate is 1-2 FPS or so. It seems background execution doesn't help much. I suspect it's because background execution is great when the task being executed is not CPU-intensive but getting EntryIDs is heavy operation and thus severely interferes with UI.

I also have some legacy C++/COM code which does the same and it's slow as well. Looks like .NET interop is not the root cause of the issue. Maybe there is another API call which I should use for that?

I'm currently testing with Outlook 2010 64-bit.

2
It seems Outlook becomes slower when you have lots of Outlook.ContactItem objects at the same time. If I split the "for" loop in chunks and release the objects with ReleaseComObject as soon as possible (so, no more than 200 objects exist simultaneously), performance increases by 3-4 times and Outlook becomes more responsive. Better but still quite slow (and performance no longer improves if I reduce chunks to smaller values).Alex

2 Answers

2
votes

Use MAPITable.GetTable to retrieve properties from multiple items in a single call without opening them at all (which is really expensive).

Secondly, OOM cannot be used on a secondary thread - it's never been supported, and Outlook 2016 will raises an exception as soon as it detects that it is being used from anything but the main UI thread. Only Extended MAPI (C++ or Delphi) is thread-safe. You can also use Redemption (any language) and its RDO family of objects - it is 100% Extended MAPI based and can be used from a secondary thread. In particular, you can use

RDOFolder.Items.MAPITable.ExecSQL("SELECT EntryID FROM FOLDER WHERE MessageClass = 'IPM.Contact' ")

to retrieve entry ids of all contacts as a recordset.

1
votes

OK, got it. Splitting "for" in chunks and keeping the number of simultaneously existing Outlook.ContactItem objects (like in my comment above) was one part of the optimization but more important is another thing.

The docs say you cannot use Items.SetColumns("EntryID") but never explain why. Actually, you cannot pass it there because EntryID is always returned! So, passing any other light-weight property (I used "Initials") will do the trick. It returns the object with just EntryID and Initials fields set and this improves performance by 4-5 times.

Combined with chunks optimization, I now have everything completed in 6 seconds in the background thread (with the main thread it's even faster).