← All articles

Generating a DTO from a small schema model

A generator saves repetition only when its output remains predictable and compilable. This small example validates names and limits types before generating a record.

t.Turgay
Generating a DTO from a small schema model

Repeating the same columns in DTOs, models and service parameters usually creates an uninteresting but expensive bug: one name changes and another file is forgotten. Schema-driven generation reduces that repetition. It does not become reliable merely by inserting database column names into C# text. A desktop generator in my archive contains model and bootstrap templates; this example isolates a small part of that approach so its contract can be inspected.

Constrain the language being generated

The input contains Id/int and Title/string pairs. The expected output is public sealed record ItemDto(int Id, string Title);. Run the example in a .NET 10 console project with ImplicitUsings and Nullable enabled. No database connection is needed to test this first boundary between a schema model and emitted source.

var columns = new[] { (Name:"Id", Type:"int"), (Name:"Title", Type:"string") };
static bool Identifier(string name) =>
    name.Length > 0 && (char.IsLetter(name[0]) || name[0] == '_') &&
    name.All(c => char.IsLetterOrDigit(c) || c == '_');
if (columns.Any(c => !Identifier(c.Name))) throw new ArgumentException("Invalid name");
var allowedTypes = new HashSet<string> { "int", "string", "decimal" };
if (columns.Any(c => !allowedTypes.Contains(c.Type))) throw new ArgumentException("Invalid type");
var fields = string.Join(", ", columns.Select(c => $"{c.Type} {c.Name}"));
Console.WriteLine($"public sealed record ItemDto({fields});");

The checks address different concerns. Identifier checks a rough naming pattern; allowedTypes limits the type expressions the generator accepts. A random source expression in the Type field is therefore not treated as normal input. Printing the result is useful for inspection, but compilation should ultimately decide whether the generated file is valid C#.

A plausible name can still produce invalid code

This Identifier helper is not a C# parser. The word class contains letters but is reserved. Two Id fields each pass the character check but cannot both become parameters in the same record. Display Name fails earlier because it contains a space. A practical generator needs keyword handling, duplicate detection and collision checks after name normalization.

Silent cleanup introduces another failure mode. Converting both “Order Id” and “Order-Id” to OrderId loses the distinction between their sources. Report the source name, generated name and collision reason together. Roslyn language services are a stronger foundation for C# naming rules than indefinitely expanding a hand-written character predicate.

Type mapping includes nullability and database semantics

A real schema includes nullable columns, decimal precision and scale, date types and length constraints. A required text column and a nullable text column should not emit indistinguishable contracts. SQL Server uniqueidentifier and PostgreSQL uuid may map to a similar .NET type, but provider rules should live in one intermediate model rather than being repeated in every output template.

That model separates schema collection from rendering: first produce column definitions, then generate a DTO, entity or mapping file from the same definitions. A provider change then has a clear boundary. The three allowed types in this sample are intentionally narrow; it does not implement nullable analysis or a complete database-to-.NET type map.

Make ownership of generated files explicit

Regeneration should not erase business behavior someone added by hand. I keep generated files in a defined location and extensions in separate partial or service files. Output filenames and paths also require a policy: a valid C# member name is not proof that a user-controlled path is safe. I would not derive an arbitrary project directory directly from submitted input.

Deterministic output needs a deliberate column order. Repeating generation for the same schema should produce the same source. Machine-specific paths and changing timestamps create noisy diffs without describing a schema change. Writing to a temporary location and validating the result before replacing the destination also reduces the chance that an interrupted run leaves otherwise working code incomplete.

Test both the generator and its output

Alongside the normal two-column case, exercise empty names, keywords, duplicate names, unsupported types and nullable columns. A textual snapshot can detect an unexpected template change; compiling representative output catches language errors; comparing with the schema catches missing data. These checks answer different questions and become more valuable as the generator grows beyond a single record declaration.

C# identifier rules; Reverse engineering models from a schema.

✳ 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 ✳