Every inference feature eventually has to answer one question: who wins when two of us want the same member? For coherence, the sharp version of that question is attributes. A model that annotates its email is both telling the validator something and, now, overlapping with the persona.
public sealed class Contact
{
public string FirstName { get; set; } // persona facet
public string LastName { get; set; } // persona facet
[EmailAddress] // an attribute, on a persona facet
public string Email { get; set; }
}
The persona wants Email to be maria.garcia@..., coherent with the name. The [EmailAddress] attribute wants "an email." A registered custom provider might want it too. So where does the persona sit in the precedence chain?
Three options
A, conservative: persona below attributes.
[EmailAddress] selects a generic email generator -> "k9x@host.net"
Valid, but NOT coherent. Annotating your model quietly loses coherence.
B, aggressive: persona above providers and attributes.
Persona's coherent email wins -> "maria.garcia@ex.com"
Coherent, but it silently overrides a provider you deliberately registered.
C, compose: persona at the automatic tier.
It FULFILLS a select attribute coherently ([EmailAddress] is satisfied by a
coherent, valid email), is NARROWED by a constraint attribute ([StringLength]),
beats plain semantic/type, and loses to explicit rules and providers.
The choice
C, compose. It is the only one that maintains coherence across annotated models, which is the common shape for seed data, while still letting an explicit provider win, which is what a user most clearly meant.
What made C feel principled rather than like a special case is that it rests on a distinction the API already had: semantic attributes select a generator, constraint attributes narrow values. [EmailAddress] selects "email," and a coherent email is an email, so the persona fulfills it. [StringLength] narrows, so the persona facet gets narrowed to fit. The persona did not need a new tier bolted on top of the precedence list. It slotted into a model that was already there.
The conservative option lost because it punishes you for annotating your model, which is exactly the kind of surprise that erodes trust. The aggressive option lost because "automatic inference silently overrode the provider I registered" is a worse surprise. Compose avoids both: the explicit thing always wins, the automatic thing fills in coherently, and the attribute gets honored either way.
Takeaway
Precedence is where features negotiate, and the best outcome is usually compose, not override. Before you add a new priority tier, look for the distinction your system already makes (here, select versus narrow) and slot the new feature into it. A feature that fits the existing model is easier to learn and far easier to
trust than one that fights it.
What's next
The persona can now bind, draw deterministically, and negotiate precedence. Two things remain before it is presentable: it has to be fully believable, and it has to be honest about what it did. Next post: a believable person, and making it honest.
Top comments (0)