A configurable validator for column-separated text files (CSV-like). Available as a CLI (validate) and a NuGet library (CsvValidator). See README.md for end-user usage and the config format.
src/Validate.Lib— the validation library. Targets netstandard2.0; packaged as NuGetCsvValidator. All core logic lives here.src/Validate— the CLI executable (assemblyvalidate). Targets net10.0. A thin wrapper over the library; owns argument parsing, console output, and the process exit code.test/ValidateTests— MSTest tests + fixtures underData/. Targets net10.0.
Keep library code netstandard2.0-compatible; only the CLI and tests get net10.0 features.
dotnet test FormatValidator.sln
.NET 10 SDK. Run the full suite before and after any change — the baseline must stay green.
Config-driven pipeline: JSON config → ValidatorConfiguration (via JsonReader/Newtonsoft) → ConfigurationConvertor → ConvertedValidators → Validator.
- Validation is cell-scoped and column-indexed. Each
IValidator.IsValid(string)checks one column's value. Validators are grouped per column index inRowValidator._columns(aValidatorGroup[]sized to the highest configured column index). RowValidator.IsValid(row)splits the row (ColumnSplitter) and runs the per-column validators.Validator.Validate(reader)streams row-by-row (IEnumerable, no whole-file buffering) and excludes the header row from content validation whenhasHeaderRowis set.- Expected column width (strict mode) =
_columns.Length= the highest configured column index. This is the single source of truth — do not re-derive width from config dictionary order. - Error model:
RowValidationError(Row,Content,Errors[]) aggregatesValidationError(Column,AtCharacter,Message). Both are 1-based; a row-level error with no specific cell (e.g. column-count) usesColumn/AtCharacter=0. - CLI:
Program.Mainreturns anintexit code;Runreturns0(pass) /1(fail), derived from the sameerrors.Countsignal the UI uses for PASSED/FAILED. Arg-parse failure → non-zero. Human output goes throughConsoleUserInterface : IUserInterface; the exit code is the machine signal.
- Comments & naming: prefer encoding intent in names, types, and method structure — a comment that explains what the code does is a smell; extract a well-named method instead, and reserve comments for why the code can't express. For doc comments: XML
///on public API only; internal/private members use plain//or none (no XML docs on non-public members). - Surface: internal-by-default; keep new types and members as tightly scoped as possible. To test internals, use
[assembly: InternalsVisibleTo("ValidateTests")](already present on bothValidate.LibandValidate) — do not widen something topublicjust to test it. - Abstraction: prefer a little duplication over a premature abstraction (rule of three). Don't grow a class's API to remove a small or one-off duplication.
- Single source of truth for any value that must stay consistent across code paths (see expected-width above).
- Console streams: human-readable / diagnostic output goes to stderr (
Console.Error); stdout is reserved for machine-readable output (e.g.--output). Don't put human text on stdout. - Tests: MSTest (
[TestClass]/[TestMethod]); match the style intest/ValidateTests(e.g.Integration/FunctionalTests.cs). Fixtures live undertest/ValidateTests/Dataand are registered as<Content>(copied to output) inValidateTests.csproj— new fixture files must be added there. For small cases prefer inline strings fed throughStreamSourceReader(over aMemoryStream) to avoid fixture churn.
- Fixture line endings: configs use
"rowSeperator": "\r\n", so fixture CSV files must be CRLF — an LF file parses as a single row. - Dictionary key order:
ValidatorConfiguration.Columns/ConvertedValidators.ColumnsareDictionary<int,...>..Keys.Last()returns enumeration (insertion) order, not the maximum key — use the highest index /_columns.Length, neverKeys.Last(), for width.