I found a weird behaviour (a bug?) in .NET Framework and .NET when it comes to DataColumn renaming. Here's how to reproduce it.
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?
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
.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With