The first question I'd ask is: do I really need to do this at all? The typed DataSet designer already gives you a tool for defining the mapping between a stored procedure and a DataTable. If you design your DataSet with care, you already have a Fill method for every DataTable. Does it make sense to reinvent that wheel?
I think it might. It's really cool that there's a way to maintain that mapping, but everything in that mapping is frozen at compile time. If you want to change the mapping, you need to rebuild your assembly. Also the typed DataSet design doesn't deal with stored procedures that return multiple result sets. If you want to generically map parameters and values, you have to use reflection to get the argument lists from the Fill methods. It may be that if you look at those factors (and others I'm not thinking of), working with the existing tool isn't the way to go.
In that case, it seems to me that your goal is to be able to populate a DataSet from a series of stored procedures with code that knows as little as possible about the implementation details. So this is a process that's going to be driven by metadata. When you have a process driven by metadata, what's going to matter the most to you in the long run is how easy it's going to be to maintain the metadata that the process uses. Once you get the code working, you probably won't touch it very much. But you'll be tweaking the metadata constantly.
If I look at the problem from that perspective, the first thing I think to do is design a typed DataSet to contain the metadata. This gives us a bunch of things that we'd otherwise have to figure out:
- a persistence format
- a straightforward path to building a bound UI
- an equally straightforward path to persisting the metadata in a database if we decide to go down that road
- an object model for navigating the data.
In this DataSet, you'd have a DataSetType table, keyed on the Type of each typed DataSet you intend to be able to populate. It would have a child StoredProcedures table, with a row for each SP that gets called. That would have two child tables, Parameter and DataTableType. There would be one DataTableType row, ordered by ordinal position, for each result set that the SP's expected to return. The DataTableType table would have a child ColumnMapping table. It's in that table that you'd maintain the mappings between the columns in the result set and the columns in the table you're populating.
Make sure all of your DataRelations are Nested, and that you've given rational names to the relations. (I like FK_childtablename_parenttablename
.)
Once you have this, the class design becomes pretty straightforward. The class has a reference to the metadata DataSet, the Connection, etc.,, and it exposes a method with this signature:
public void FillDataSet(DataSet targetDs, Dictionary<string, Dictionary<string, KeyValuePair<string, string>> parameterMap);
You start by using the targetDs's Type to find the top-level DataSetType row. Then all of the private methods iterate through lists of DataRows returned by DataTable.GetChildRows(). And you add an event or two to the class design, so that as it performs the operation it can raise events to let the calling application know how it's progressing.
Probably the first place I'd expect to refactor this design is in giving me more fine-grained control over the filling process. For instance, as designed, there's only one set of SPs per typed DataSet. What if I only want to fill a subset of the DataSet? As designed, I can't. But you could easily make the primary key of the DataSetType table two-part, with the parts being DataSet type and some string key (with a name like SPSetName, or OperationName), and add the second part of the key to the FillDataSet argument list.