4

I found a weird behaviour (a bug?) in .NET Framework and .NET when it comes to DataColumn renaming. Here's how to reproduce it.

  1. Take a DataColumn in a DataTable
  2. Rename it so that you only change its casing
  3. Rename it again to something different
  4. Now the column will be available both under the old and under the new name
  5. If you rename it back to the old name, you'll get a DuplicateNameException

The problem seems related to the DataColumnCollection's internal dictionary not getting updated when a column gets renamed to only change its casing.

DataTable table = new DataTable();
table.Columns.Add("foo", typeof(string));
// The DataTable contains a single column named "foo"
// The dictionary _columnFromName contains a single entry having its key equal to "foo"

table.Columns["foo"].ColumnName = "FOO";
// The DataTable now contains a single column named "FOO"
// The dictionary _columnFromName did not get updated, so the entry key STILL equal to "foo"

table.Columns["FOO"].ColumnName = "bar";
// The DataTable now contains a single column named "bar"
// Here the DataColumnCollection registers the new columnname "bar", but fails the unregistration of the column name "FOO", because the dictionary does not contain a key equal to "FOO".
// So, the dictionary _columnFromName now contains TWO entries having their keys equal to "foo" and "bar"

// In fact, the column is still available under the old name ...
Console.WriteLine(table.Columns["foo"]);

// ... and of course it is also available under the new name "bar"
Console.WriteLine(table.Columns["bar"]);
        
// Now, this will throw a DuplicateNameException, because the old dicionary key is still present
table.Columns["bar"].ColumnName = "foo";

Here's a .NET Fiddle targeting .NET Framework 4.7.2. You can change it to .NET 7 and you will still encounter this problem. https://dotnetfiddle.net/vhoV6X

Has anyone else encountered this behaviour? Is it intended? Is it known to Microsoft?

Charlieface
  • 52,284
  • 6
  • 19
  • 43
dabon
  • 41
  • 3
  • At first I thought about the CaseSensitive property of the DataTable. The default is false so I tried setting it to true before adding columns. But it seems that the problem is still there also with the CaseSensitive property set to true. – Steve May 23 '23 at 20:40

2 Answers2

3

The code for the ColumnName setter uses a string.Compare with ignoreCase: true in order to check whether to call table.Columns.RegisterColumnName.

if (String.Compare(_columnName, value, true, Locale) != 0) {
// skip... 
    table.Columns.RegisterColumnName(value, this);
    if (_columnName.Length != 0)
        table.Columns.UnregisterName(_columnName);
}

So only if the name has changed in a case-insensitive way then the Columns of the table is updated. This gives rise to a bug where you can change it first to a name which is similar (and the UnregisterName does not happen), then a name which is different (where the UnregisterName fails because it can't find it).

The UnregisterName function is as follows:

    internal void UnregisterName(string name) {
        columnFromName.Remove(name);

        if (NamesEqual(name, MakeName(defaultNameIndex - 1), true, table.Locale) != 0) {
            do {
                defaultNameIndex--;
            } while (defaultNameIndex > 1 &&
                     !Contains(MakeName(defaultNameIndex - 1)));
        }
    }

The bug is on the first line: there is no check if Remove has failed to find the column in the dictionary.

Really what should have happened is that the dictionary columnFromName should have been initialized with a case-insensitive comparer StringComparer.OrdinalIgnoreCase.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
1

I can't find any official documentation about this behaviour, but my assumption is that the name of a column is not supposed to change once it has been set. If Microsoft were re-implementing the class now without needing backwards compatibility, I expect they would make the property 'init' instead of 'set'.

Let's say you are allowed to rename a column. If the datatable already contains rows, what should happen? Should the values be moved to the new column name, or stay the same?

A similar example is HashSet. It can't stop you from modifying an object that has already been added, even though doing this could change the hash and so break the functionality of the hash set (this one is well documented though).

Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62
  • 2
    OK, changing the name of a column after adding it to the table is a bit 'illogical' but the problem exists. Searching in the referencesource it seems originated in a string.Compare inside the ColumnName property using the parameter for _ignoreCase_ set to true. – Steve May 23 '23 at 20:56
  • 1
    Looks to me like an out-and-out bug. It *does* allow you to change the name, and there's loads of code for it, but they don't take into account that a case-insensitive value won't be found in the column dictionary. See my answer – Charlieface May 23 '23 at 21:40