‘Bug’ in ASP 3.0 Application.Contents iterator causes undesired deletion patterns when Application.Contents.Remove() is called from within a for each / for loop

System Requirements:

  • Windows NT 4.0 Server SP4+
  • Windows Server 2000
  • Windows Server 2003
  • Windows Server 2008, R2
  • Windows Server 2012, R2
  • Windows Server 2016
  • Windows Server 2019
  • Windows 2000 Professional
  • Windows XP
  • Windows Vista
  • Windows 7
  • Windows 8, 8.1
  • windows 10
  • IIS 4.0, 5.0, 5.1, 6.0, 7.0, 7.5, 8.0, 10.0
  • ASP 3.0 (Classic)

The Problem:

I remember, long ago in approximately 2001 – in my less competent days – fighting to make something work and ultimately concluded that it was a hapless endeavour and ultimately went about it in a different way. The task was to clear down all but a small number of elements from the ASP 3.0 Application.Contents object.

What I concluded then, is something that I’ve only just re-remembered now after finally making a determined effort to hunt down a bug in a module on HPC:Factor – which is being used elsewhere – and in which a recent change brought the issue back to light.

There is an iterator issue come bug (depending on your point of view) in the ASP 3.0 Application object.

More Info

We’ll lead by example with this one. After spending an hour or so reacquainting myself with the problem while fixing (read making more robust) the HPC:Factoor class module, a fairly simple process can be used to demonstrate it. Whether or not you see this as a natural feature, or a sincere bug is something that I’ll leave to you. There are always ways around this sort of thing, so I guess that what counts is whether you think it should be fixed in the iterator or by the end user.

Take the following code

Option Explicit
Dim strKey
Application.Contents.RemoveAll()
Application.Contents("one") = "a"
Application.Contents("two") = "b"
Application.Contents("three") = "c"
Application.Contents("four") = "d"
for each strKey in Application.Contents
  Response.Write strKey & " == " & Application.Contents(strKey) & "<br />"
next

It’s obviously going to print out the following

one == a two == b three == c four == d

So what if we now do this:

for each strKey in Application.Contents
  Application.Contents.Remove(strKey)
next
for each strKey in Application.Contents
  Response.Write strKey & " == " & Application.Contents(strKey) & "<br />"
next

Clearly it should print

For dramatic effect, that’s “absolutely nothing being printed”. The application object should be completely empty.

Wrong! It prints:

two == b
four == d

What’s going on is quite simple. The for each iterator being called from the Application.Contents collection is indexed, in other words when items are added or removed they are given a numeric, integer based index in order to aid lookup.

This index becomes stateful as it initially exists at call time for the “for each” provider and its content is copied out to the iterator, By Value (ByVal). It should really be passed out By Reference (ByRef) i.e. via a Pointer.

What this means (using comments to explain the process) is that the following logic occurs:

Option Explicit
Dim strKey
Application.Contents.RemoveAll()            ' Delete all indexes, release pointers to all data
Application.Contents("one") = "a"           ' Create Index 1, Key:"One", Value:"a"
Application.Contents("two") = "b"           ' Create Index 2, Key:"two", Value:"b"
Application.Contents("three") = "c"         ' Create Index 3, Key:"three", Value:"c"
Application.Contents("four") = "d"          ' Create Index 4, Key:"four", Value:"d"
' Application.Contents.Count = 4

for each strKey in Application.Contents     ' Create an iterator of the index [1 - 4]
  ' Iterator Index i = 1
  Application.Contents.Remove(strKey)     ' Remove item at index 1
  ' Index 1 removed, compact index
  ' Index 1, Key:"two", Value:"b"
  ' Index 2, Key:"three", Value:"c"
  ' Index 3, Key:"four", Value:"d"
  ' Application.Contents.Count = 3
  ' Move to NEXT
  ' Iterator Index = 2 (i = (i + 1))
  Application.Contents.Remove(strKey)     ' Remove item at index 2
  ' Index 2 removed, compact index
  ' Index 1, Key:"two", Value:"b"
  ' Index 2, Key:"four", Value:"d"
  ' Application.Contents.Count = 2
  ' Move to NEXT
  ' Iterator Index = 3 (i = (i + 1))
  ' 3 is greater than 2 (the index is > count), exit

The problem is that the Index is being compacted on a successful call to .Remove(). The count of the number of items in Application.Contents is being updated to reflect the correct number of items, but the iterator isn’t being told i = (i – 1) after the successful completion of the Remove() method.

The same thing happens if you use “for” rather than “for each”:

Option Explicit
Dim i
Application.Contents.RemoveAll()
Application.Contents("one") = "a"
Application.Contents("two") = "b"
Application.Contents("three") = "c"
Application.Contents("four") = "d"
for i = 1 to Application.Contents.Count
  Application.Contents.Remove(i)
next

This also results in data still remaining inside the Application Object due to the same error, except here we are directly calling the iteration number ourselves via i so we also get 2 and 4 left in the collection as with calling .Remove() from the “for each”.

If the Application.Contents.Remove method supported a success/failure return type – for example a boolean true for item removed and boolean false for no such item in collection, then the fix would be simple:

for i = 1 to Application.Contents.Count
  if (Application.Contents.Remove(i)) then
    i = (i - i)
  end if
next

Sadly the method doesn’t support a return type.

The Fix

This bug means that there are only two ways to deal with it The first way would be to iterate across the collection, store the Keys in an array and then in a second pass remove all of the items that you want to delete by using an external array.

It does the job and allows you to continue to use keys, but why use two loops when you can use one? In the knowledge that the following is true:

  1. The index is compacting
  2. The iterator is not being reduced by 1 after a successful call to .Remove()

The second and simplest approach to solve the problem is to force the for loop to decrement it for you. In other words, reverse iterate instead of forward iterate through the collection.

Dim i

Application.Contents.RemoveAll()
Application.Contents("one") = 1
Application.Contents("two") = 2
Application.Contents("three") = 3
Application.Contents("four") = 4
for i = Application.Contents.Count to 1 step -1
  Application.Contents.Remove(i)
next

for i = 1 to Application.Contents.Count
  Response.Write Application.Contents.Key(i) & " == " & Application.Contents.Item(i) & "<br />"
next

By going backwards, the index is decremented and so is the external iterator, meaning that they keep in sync with each other.

To adapt this further, if you only want to remove certain items from the collection and want to delete based upon the key, use the following.

Dim i
Application.Contents.RemoveAll()
Application.Contents("one") = 1
Application.Contents("two") = 2
Application.Contents("three") = 3
Application.Contents("four") = 4
for i = Application.Contents.Count to 1 step -1
  if ((Application.Contents.Key(i) = "one") OR (Application.Contents.Key(i) = "three")) then
    Application.Contents.Remove(i)
  end if
next

for i = 1 to Application.Contents.Count
  Response.Write Application.Contents.Key(i) & " == " & Application.Contents.Item(i) & "<br />"
next

Using our example above, the output will correctly be:

two == b
four == d