Showing posts with label LINQ. Show all posts
Showing posts with label LINQ. Show all posts

The Dark Side of Covariance

What’s wrong with the following code?

var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
...
if (names.Contains(sqlCommand.ExecuteScalar())

This  code is intended to check whether the result of a SQL query is contained in a case-insensitive collection of names.  However, if you run this code, the resulting check will be case-sensitive.  Why?

As you may have guessed from the title, this is caused by covariance.  In fact, this code will not compile at all against .Net 3.5. 

The problem is that ExecuteScalar() returns object, not string.  Therefore, it doesn’t call HashSet<string>.Contains(string), which is what it’s intending to call (and which uses the HashSet’s comparer).  Instead, on .Net 4.0, this calls the  Enumerable.Contains<object>(IEnumerable<object>, string) extension method, using the covariant conversion from IEnumerable<string> to IEnumerable<object>.  Covariance allows us to pass object to the Contains method of any strongly-typed collection (of reference types).

Still, why is it case-sensitive?  As Jon Skeet points out, the LINQ Contains() method is supposed to call any built-in Contains() method from ICollection<T>, so it should still use the HashSet’s case-insensitive Contains().

The reason is that although HashSet<String> implements ICollection<string>, it does not implement ICollection<object>.  Since we’re calling Enumerable.Contains<object>, it checks whether the sequence implements ICollection<object>, which it doesn’t.  (ICollection<T> is not covariant, since it allows write access)

Fortunately, there’s a simple fix: just cast the return value back to string (and add a comment explaining the problem).  This allows the compiler to call HashSet<string>.Contains(string), as was originally intended.

//Call HashSet<string>.Contains(string), not the
//covariant Enumerable.Contains(IEnumerable<object>, object)
//http://blog.slaks.net/2011/12/dark-side-of-covariance.html
if (names.Contains((string)sqlCommand.ExecuteScalar())
(I discovered this issue in my StringListConstraint for ASP.Net MVC)

Binding to lists of DataRows

.Net DataTables can be very useful when writing data-driven applications.  However, they have one limitation: There is no obvious way to databind a grid (or other control) to an arbitrary list of datarows from a table.
You can bind to an entire table directly by setting a DataSource to the DataTable itself, and you can bind to a subset of a table by creating a DataView with a filter. 

In general, you cannot bind to an IEnumerable<T> (eg, a LINQ query); the databinding infrastructure can only handle an IList (non-generic) or an IListSource.  This is true for any kind of datasource.  Therefore, to bind to any LINQ query, you need to call .ToList().  (Or .ToArray())

However, when binding to a DataTable, you  can’t even use a List<DataRow>.  If you try, you’ll get four columns (RowError, RowState, Table, and HasErrors) and no useful information.  This happens because the List<DataRow> doesn’t tell the databinding infrastructure about the special properties of the DataRows.  To understand the problem, some background is necessary

Databinding is controlled by the ListBindingHelper and TypeDescriptor classes.  When you bind to a list, the ListBindingHelper.GetListItemProperties method is called to get the columns in the list.  If the list implements the ITypedList interface, its GetItemProperties  method is called.  Otherwise, it will use TypeDescriptor to get the properties of the first item in the list.  (this uses reflection)

The DataView class (which DataTable also binds through, using IListSource) implements ITypedList and returns DataColumnPropertyDescriptors that expose the columns in the table.  This is why you can bind to a DataView or DataTable and see columns.  However, when you bind to a List<DataRow>, there is no ITypedList that can return the columns as properties.  It therefore falls back on reflection and shows the physical properties of the DataRow class.

To solve this issue, you need to wrap the list in a DataView so that you can take advantage of its ITypedList implementation.  You can do that using the AsDataView() method.  This method is only available on the DataTable  and EnumerableRowCollection<T> classes; it cannot be called on an arbitrary LINQ query.  You can only get an EnumerableRowCollection<T> by calling special versions of the Cast, OrderBy, Where, and Select methods from a DataTable. 

Therefore, you can databind to a simple LINQ query by calling AsDataView() on the query.  To bind to a List<DataRow>, or to a more complicated query, you can use an ugly hack:

List<DataRow> list = ...;
grid.DataSource = table.AsEnumerable()
                       .Where(list.Contains)
                       .AsDataView();

The  AsEnumerable() call is not needed for typed datasets.

You can also call CopyToDataTable(), which will works on an arbitrary IEnumerable<DataRow>.   However, it makes deep copies of the rows, so it isn’t helpful if you want the user to update the data, or if you want the user to see changes made (in code) to the original datarows.