It's the DataProjectorToDataTable class. It creates the columns when the first row is added. This is a bit silly, admitted, as the columns have to be there eventually, and the columns are created from the projectors anyway, so no row is required. I think this is left-over design from the time when the projector did use the rows to create the columns, but that's not needed anymore, it can create the columns right away.
If you fetch a typed list with an empty datatable and you disable the string cache it will create the columns, however if you keep the string cache, it will use our own datatable filler as well, and will not create the columns if the table is empty.
I'm not sure what to do though: changing it now will break application which assume an empty table and there's shoddy code in place to check this in the user's application.
So I'm reluctant to change this now. I'll add the change for v4. THis will then also affect typedlist fetches which would now return an empty datatable.
You can work around this in another way btw. Use the FetchAsProjection overload which accepts a projector and pass in a modified DataProjectorToDataTable instance (one which calls CreateColumns in the Ctor)
There might be sideeffects as well, in edge cases with typeconverters, as the datatable filler re-uses column names if they exist instead of the ones from the resultset. In the case they differ from the ones in the projectors, this could mean a problem, but I don't know how big (or if it occurs at all, just thought about this when looking at the code).
I.o.w.: we'll postpone this to v4 so we can test it properly and document the changes.