Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DataColumn renaming to only change casing causes a weird behaviour

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?

like image 852
ColdSir Avatar asked Sep 05 '25 03:09

ColdSir


2 Answers

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.

like image 170
Charlieface Avatar answered Sep 07 '25 22:09

Charlieface


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).

like image 24
Andrew Williamson Avatar answered Sep 07 '25 20:09

Andrew Williamson