Importing customers from text
A useful import accepts good records and reports bad ones with their row numbers. It needs an explicit file format, stable duplicate detection and the same validation rules as interactive entry.
This example accepts lines of the form Code|Name|Email. The code is two ASCII uppercase letters followed by four digits. Fields cannot contain the separator. This deliberately simple format has no quoting or escaped delimiters; use an existing CSV parser when those capabilities are required.
AB0042|Ana Rivera|ana@example.com
AB0043|Leon Chen|leon@example.com
AB0042|Duplicate Customer|other@example.com
invalid row
The report and importer
The Customer and CustomerValidator declarations are from validation. The import creates fresh customer identifiers; the input code is an import-local duplicate key, not a persisted Customer.Id.
import Global.System
import Global.System.Collections.Generic
import Global.System.IO
import Global.System.Linq
public class ImportReport
public BatchName as String = ""
public Accepted as List <of Customer> = new List <of Customer>()
public Rejected as List <of String> = new List <of String>()
end class
public class CustomerImporter
private readonly validator as CustomerValidator
public constructor(customerValidator as CustomerValidator)
require customerValidator is not nothing
validator = customerValidator
end constructor
public action Read(path as String)
returns ImportReport
require path is not whitespace
let report = new ImportReport()
let filename = Path.GetFileName(path)
if filename is like "customers-{{let batch = *}}.txt" then
report.BatchName = batch
else
throw new ArgumentException("Expected a customers-<batch>.txt filename")
end if
let seen = new HashSet <of String>(StringComparer.OrdinalIgnoreCase)
let rowNumber = 0
for_each line in File.ReadLines(path)
rowNumber += 1
if line is whitespace then continue
if line is match_case_sensitive "^(?<code>[A-Z]{2}[0-9]{4})[|](?<name>[^|]+)[|](?<email>[^|]+)$" then
let customer = new Customer()
customer.Id = Guid.NewGuid()
customer.Name = name.Trim()
customer.Email = email.Trim()
let validation = validator.Validate(customer)
if not validation.IsValid then
for_each failure in validation.Errors
report.Rejected.Add("Row {{rowNumber}}: {{failure.PropertyName}} — {{failure.ErrorMessage}}")
end for_each
elseif not seen.Add(code) then
report.Rejected.Add("Row {{rowNumber}}: duplicate code {{code}}")
else
customer.IsActive = true
report.Accepted.Add(customer)
end if
else
report.Rejected.Add("Row {{rowNumber}}: expected Code|Name|Email")
end if
end for_each
return report
end action
public action NamesForReview(report as ImportReport)
returns List <of String>
require report is not nothing
return report.Accepted.OrderBy(.Name).Select(.Name).ToList()
end action
end class
The filename wildcard introduces batch only within its successful branch. The fixed regex introduces code, name and email within the record's successful branch. A dynamic regex would not introduce unknown locals into the source scope.
The explicit is match_case_sensitive predicate enforces the uppercase code format. Ordinary is match ignores case. HashSet nevertheless uses an explicit ordinal case-insensitive comparer for duplicate keys. Source identifier casing and data comparison are separate choices; DirectSharp's case-insensitive names do not automatically change every imported .NET collection's equality policy.
File.ReadLines enumerates the file lazily using the normal .NET API. for_each uses and disposes its enumerator. File-open and read failures propagate; they are not converted into malformed-row messages. This prevents a broken or partially read file from being reported as a successfully completed import.
The seen set receives a code only after validation succeeds. An invalid first record therefore does not prevent a later corrected record with the same code from being accepted. Each rejected validation can add several messages; Rejected.Count counts diagnostics, not necessarily rejected records.
NamesForReview demonstrates normal LINQ extension calls. OrderBy and Select use typed selectors, and ToList materializes the result. The default .NET String ordering used by OrderBy is culture-sensitive; supply an explicit comparer when exported order must be invariant across machines.
Reporting and persistence
public class ImportExample
public static action Run(path as String)
let importer = new CustomerImporter(new CustomerValidator())
let report = importer.Read(path)
Global.System.Console.WriteLine("Batch {{report.BatchName}}: {{report.Accepted.Count}} accepted")
for_each issue in report.Rejected
Global.System.Console.WriteLine(issue)
end for_each
for_each name in importer.NamesForReview(report)
Global.System.Console.WriteLine("Ready: {{name}}")
end for_each
end action
end class
Activation here is the sample's import policy. A production onboarding process may instead retain inactive customers until consent or verification is complete. Persistence is deliberately a following step: choose whether an import is transactional or permits partial commits before calling CustomerRepository.AddAsync.
Test blank lines, invalid format, trimmed fields, duplicate codes, multiple validation errors and unreadable files. Treat record diagnostics and infrastructure failures as distinct outcomes.