← All articles

Converting rows with actionable validation errors

An imported cell is not automatically a valid domain value. Convert each row deliberately when quantities may contain text, empty cells or invalid input.

t.Turgay
Converting rows with actionable validation errors

Reading a Quantity cell may take one line of code. Deciding what to persist when it contains 12, whitespace, a formula result or the word “unknown” is an application decision. The table conversion helpers in my archive point to this boundary: reading a value and accepting a value are separate operations.

Know the source schema before casting

The example deliberately defines Quantity as a string column. Its three rows contain “12”, “oops” and DBNull. The target contract accepts a non-negative integer. Naming those two schemas first makes the conversion behavior testable. This .NET 10 console example uses System.Data and System.Globalization; reading an actual Excel workbook is a separate input step.

using System.Data;
using System.Globalization;
var table = new DataTable();
table.Columns.Add("Quantity", typeof(string));
table.Rows.Add("12"); table.Rows.Add("oops"); table.Rows.Add(DBNull.Value);
for (int i=0; i<table.Rows.Count; i++)
{
    var text = table.Rows[i].Field<string>("Quantity");
    var valid = int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var quantity) && quantity >= 0;
    Console.WriteLine(valid ? $"row {i+1}: {quantity}" : $"row {i+1}: invalid quantity");
}

Field<string> returns null for DBNull in this text column. TryParse then rejects it through a normal validation result. It is not a general conversion method for a column that is actually typed as int or decimal. The import reader's output schema needs to be understood before applying the same accessor to every data source.

Parsing and business acceptance answer different questions

A successful int.TryParse says that the input represents an integer within the type's range. The quantity >= 0 condition says that the represented value is acceptable as a quantity here. “-3” has a valid integer format but violates the business rule. “2147483648” fails earlier because it does not fit in an int. Keeping these reasons separate can make the error report more useful to the person correcting the file.

NumberStyles.Integer can accept surrounding whitespace and a sign. It does not make decimal fractions part of the contract. I would not silently turn 12,5 into approximately twelve. If fractional quantities are valid, the target model and parser should both move to decimal rather than adding truncation to an integer import.

The file format determines the culture

Using the machine's current culture can make the same file behave differently on another computer. InvariantCulture is explicit in this example. A Turkish decimal format can also be supported, but it should be a documented import format with a consistent reader. Converting an already numeric spreadsheet cell to a localized string and then parsing it again introduces an unnecessary and potentially lossy round trip.

Errors must point back to the input

The message row 2: invalid quantity at least identifies the failing record. With a header row, the second data record may actually be spreadsheet row three; preserve that source row separately. A useful import error also includes the column, expected kind of value and rejection reason. It need not dump the entire row or unrelated personal data into logs.

Replacing invalid values with zero destroys information if zero is itself valid. Missing data then becomes indistinguishable from a real zero quantity. A correction queue may preserve original input under an explicit policy, but fabricated defaults should not be written into the normal business table simply to make the import finish.

Decide what happens after validation

This example prints conversion results; it does not persist them. For an all-or-nothing import I would collect the conversion results first and open a transaction only after validation succeeds. A partial-acceptance import instead needs clear accepted and rejected counts. Persisting the first row inside the loop and failing on the last row does not automatically establish either policy.

Exercise zero, negative one, surrounding whitespace, integer boundaries and an empty cell. The sample's expected outcome is twelve for the first record and errors for the remaining two. That small matrix establishes the contract before a large input file makes individual failures harder to inspect.

DataRowExtensions.Field; Int32.TryParse and NumberStyles.

✳ You have reached the end of this orbit.
Explore more articles →
turgay.A little code, a few pixels, a lot of curiosity.© 2026 · Made on Earth ✳