Table of Contents

Name matching

A slot matches a column by name, case-insensitive. A nested slot carries a prefix built from the path that reached it, so its columns read as prefix + name. Names are matched from the right, one segment at a time.

public record Address(int Zip, string City) : IDbReadable;
public record Person(int Id, Address Home);

// Columns: Id | HomeZip | HomeCity
// Home is nested, so its columns take the "Home" prefix:
//   Zip  matches HomeZip
//   City matches HomeCity

The attributes below adjust which names a slot accepts.

[Alt], another accepted name

public record Person(int Id, [Alt("Name")] string Username);
// Columns: Id | Username   -> fills Username
// Columns: Id | Name       -> fills Username

Repeat it for more names:

public record Product(int Id, [Alt("Title")][Alt("Label")] string Name);
// a Name, Title, or Label column all fill Name

The prefix still applies to an alt on a nested slot:

public record Address([Alt("Postal")] int Zip, string City) : IDbReadable;
public record Person(int Id, Address Home);
// Columns: Id | HomePostal | HomeCity   -> HomePostal fills Zip

[AltSkippingSegments], match higher up by count

A nested slot can also match a column that drops some of its inner prefix segments. The count is how many segments the name spans: 1 is the normal full prefix, 2 drops the innermost one, and so on.

public record Inner([AltSkippingSegments("Code", 2)] int Code) : IDbReadable;
public record Middle(Inner Sub) : IDbReadable;
public record Outer(int Id, Middle Mid);

// Code nests as Mid.Sub, so its full prefix is "MidSub".
// Columns: Id | MidSubCode   -> matches on the full prefix, always available
// Columns: Id | MidCode      -> a span of 2 drops the innermost segment, Sub

A span of 3 drops both segments, so a bare Code column matches too:

public record Inner([AltSkippingSegments("Code", 3)] int Code) : IDbReadable;
public record Middle(Inner Sub) : IDbReadable;
public record Outer(int Id, Middle Mid);

// Columns: Id | Code   -> fills Mid.Sub.Code, both segments dropped

[AltUpTo], match higher up by name

Same idea, but you name the segment to climb back to instead of counting.

public record LayerOne(int First, LayerTwo Two);
public record LayerTwo([AltUpTo("NotTooDeep", "Two")] int Second, LayerThree Three) : IDbReadable;
public record LayerThree([AltUpTo("SuperDeep", "Two")] int Third, [AltUpTo("SemiDeep", "Three")] int Deep) : IDbReadable;

// Columns: First | NotTooDeep | SuperDeep | TwoSemiDeep
// Second climbs up to "Two"   and matches NotTooDeep   (prefix cleared)
// Third  climbs up to "Two"   and matches SuperDeep    (prefix cleared)
// Deep   climbs up to "Three" and matches TwoSemiDeep  (prefix left at "Two")

[NoName], position and type only

Drops name matching. The slot takes the next column that fits by type, whatever its name.

public readonly struct Boxed<T>([NoName] T value) { public readonly T Value = value; }
// value fills the next column of type T, name ignored

The result shape wrappers use [NoName] on their inner value for this reason. Optional<Track> matches the same columns as a bare Track, because the wrapper adds no name of its own to prefix them.

The runtime form

For a type you cannot annotate, edit the names on its info. UpdateAltName visits every slot, and whatever INameComparer you return becomes that slot's names, so every attribute above has a runtime form. Return null to leave a slot untouched.

var info = TypeParsingInfo.GetOrAdd<Customer>();

// [Alt("PostalCode")]
info.UpdateAltName(nc => nc.GetDefaultName() == "Zip" ? nc.AddAltName("PostalCode") : null);

// [AltSkippingSegments("Code", 2)]
info.UpdateAltName(nc => nc.GetDefaultName() == "Code" ? nc.AddComparer(new NameMultiSpan("Code", 2)) : null);

// [AltUpTo("SemiDeep", "Three")]
info.UpdateAltName(nc => nc.GetDefaultName() == "Deep" ? nc.AddComparer(new NameMultiSpanKey("SemiDeep", "Three")) : null);

// [NoName]
info.UpdateAltName(nc => nc.GetDefaultName() == "Value" ? NoNameComparer.Instance : null);

AddAltName and AddComparer add to a slot's current names. Returning a comparer on its own (like NoNameComparer.Instance above) replaces them. RemoveName and RemoveComparer take one back out.

// undo the [Alt("PostalCode")] above
info.UpdateAltName(nc => nc.GetDefaultName() == "Zip" ? nc.RemoveName("PostalCode") : null);

// undo the [AltUpTo("SemiDeep", "Three")] above
info.UpdateAltName(nc => nc.GetDefaultName() == "Deep" ? nc.RemoveComparer(new NameMultiSpanKey("SemiDeep", "Three")) : null);