This method doesn't require addition of any unnecessary columns and relies on the built in functionality of the data to maintain a foreign key constraint between the parent row Id and the child row HeaderId. It doesn't rely on the order in which rows are inserted into the database and you only need two WriteToServer statements. One for the parent rows and another for all the child rows.
Your source datatable has a specified identity column, that you can set up when you create your datatable. Then you add your source (header) table plus the detail table where you want to stamp the related HeaderId to a DataSet and you create a ForeignKeyConsraint with a UpdateRule of Rule.Cascade.
When you call WriteToServer for your header table's data, the related column as specified by the ForeignKeyConstraint automatically gets updated if UpdateRule is set to Cascade.
Below are some snippets that should give you an idea of what works very well for me. For a small example solution refer to this example code on GitHub: BulkInsertHeaderDetail.
internal static class ClaimsDataSet
{
static DataSet _claimsDataSet = new DataSet();
static int _currentHeaderId = 0;
static ClaimsDataSet()
{
_claimsDataSet.Tables.Add("Header");
_claimsDataSet.Tables["Header"].Columns.Add("Id", typeof(int)).AutoIncrement = true;
_claimsDataSet.Tables["Header"].Columns["Id"].AutoIncrementSeed = 1;
_claimsDataSet.Tables["Header"].Columns["Id"].AutoIncrementStep = 1;
_claimsDataSet.Tables["Header"].Columns["Id"].Unique = true;
_claimsDataSet.Tables["Header"].Columns.Add("TreatmentDate", typeof(System.DateTime));
// Code omitted for brevity. Refer to sample app on github for all source code
_claimsDataSet.Tables.Add("Detail");
_claimsDataSet.Tables["Detail"].Columns.Add("Id", typeof(int)).AutoIncrement = true;
_claimsDataSet.Tables["Detail"].Columns["Id"].AutoIncrementSeed = 1;
_claimsDataSet.Tables["Detail"].Columns["Id"].AutoIncrementStep = 1;
_claimsDataSet.Tables["Detail"].Columns["Id"].Unique = true;
_claimsDataSet.Tables["Detail"].Columns.Add("HeaderId", typeof(int));
_claimsDataSet.Tables["Detail"].Columns.Add("TreatmentDate", typeof(System.DateTime));
// Code omitted for brevity. Refer to sample app on github for all source code
ForeignKeyConstraint foreignKeyConstraint = new ForeignKeyConstraint("HeaderIdConstraint", _claimsDataSet.Tables["Header"].Columns["Id"], _claimsDataSet.Tables["Detail"].Columns["HeaderId"]);
foreignKeyConstraint.UpdateRule = Rule.Cascade;
_claimsDataSet.Tables["Detail"].Constraints.Add(foreignKeyConstraint);
}
internal static int AddHeaderRow(string rowContent)
{
string errorMessage;
DateTime workingDate;
decimal workingAmount;
string[] commaSeparatedValues = ParseCSVLine(rowContent);
DataRow row = _claimsDataSet.Tables["Header"].NewRow();
if (DateTime.TryParse(commaSeparatedValues[0], out workingDate))
{
row["TreatmentDate"] = workingDate;
}
else
{
errorMessage = String.Format("Error converting string content to date value in {0}, Column: {1}", "Treatment Header", "TreatmentDate");
Console.WriteLine(errorMessage);
throw new FormatException(errorMessage);
}
row["Beneficiary"] = commaSeparatedValues[1];
row["ServiceProvider"] = commaSeparatedValues[2];
// Code omitted for brevity. Refer to sample app on github for all source code
_claimsDataSet.Tables["Header"].Rows.Add(row);
_currentHeaderId = Int32.Parse(row["Id"].ToString());
return _currentHeaderId;
}
internal static void AddDetailRow(string rowContent)
{
string errorMessage = "";
DateTime workingDate;
Decimal workingAmount;
string[] commaSeparatedValues = ParseCSVLine(rowContent);
DataRow row = _claimsDataSet.Tables["Detail"].NewRow();
row["HeaderId"] = _currentHeaderId;
if (DateTime.TryParse(commaSeparatedValues[0], out workingDate))
{
row["TreatmentDate"] = workingDate;
}
else
{
errorMessage = String.Format("Error converting string content to date value in {0}, Column: {1}", "Treatment Detail", "TreatmentDate");
Console.WriteLine(errorMessage);
throw new FormatException(errorMessage);
}
row["TariffCode"] = commaSeparatedValues[1];
row["TariffDescription"] = commaSeparatedValues[2];
// Code omitted for brevity. Refer to sample app on github for all source code
_claimsDataSet.Tables["Detail"].Rows.Add(row);
}
internal static void WriteToTargetDatabase()
{
try
{
string connectionString = ConfigurationManager.ConnectionStrings["claimAdminConnectionString"].ConnectionString;
using (SqlConnection destinationConnection = new SqlConnection(connectionString))
{
destinationConnection.Open();
ConnectionState destState = destinationConnection.State;
SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection);
bulkCopy.ColumnMappings.Add("TreatmentDate", "TreatmentDate");
// Code omitted for brevity. Refer to sample app on github for all source code
bulkCopy.DestinationTableName = "dbo.ClaimHeader";
bulkCopy.WriteToServer(_claimsDataSet.Tables["Header"]);
bulkCopy.ColumnMappings.Clear();
bulkCopy.ColumnMappings.Add("HeaderId", "HeaderId");
bulkCopy.ColumnMappings.Add("TreatmentDate", "ClaimDate");
bulkCopy.ColumnMappings.Add("TariffCode", "TariffCode");
// Code omitted for brevity. Refer to sample app on github for all source code
bulkCopy.DestinationTableName = "dbo.ClaimDetail";
bulkCopy.WriteToServer(_claimsDataSet.Tables["Detail"]);
destinationConnection.Close();
}
}
catch (Exception ex)
{
Console.WriteLine("Problem with bulk copy operation...");
Console.WriteLine(ex.Message);
}
}
internal static string[] ParseCSVLine(string inputString)
{
// Code omitted for brevity. Refer to sample app on github for all source code
//takes inputString and splits it into array of strings split at commas
// Convert list to array and return.
return finalValue.ToArray();
}
}