Address enrichment — one Address in, two value objects out
An entity that holds an Address almost always wants the two derived axes
beside it — where the address geocodes, and whether it’s deliverable. Wiring each
consumer to call the geocoder, then maybe a verification provider, then map both
onto the value objects, is the kind of boilerplate that drifts between modules.
Granit.AddressEnrichment is the one orchestrator that does it.
public interface IAddressEnrichmentService{ Task<AddressEnrichmentResult> EnrichAsync(Address address, CancellationToken ct = default);}
public sealed record AddressEnrichmentResult( AddressGeocoding Geocoding, AddressVerification Verification);One Address in; the two value objects an address-holding entity persists
alongside the postal text out — ready to hand to
MapAddressGeocoding / MapAddressVerification.
Two tiers, one of them optional
Section titled “Two tiers, one of them optional”EnrichAsync runs the evidence tiers
the platform defines — the two that a machine can run unattended:
flowchart LR
addr["Address"] --> svc["IAddressEnrichmentService"]
svc --> t0["Tier 0 — geocode<br/>(always)"]
svc -. "when a provider is registered" .-> t1["Tier 1 — deliverability<br/>(optional)"]
t0 --> geo["AddressGeocoding<br/>(Resolved / Failed)"]
t1 --> ver["AddressVerification<br/>(ProviderVerified / Corrected / Invalid)"]
geo --> res["AddressEnrichmentResult"]
ver --> res
Tier 0 — geocoding (always). The service geocodes
address.ToPostalAddress() through IGeocodingService.
A hit becomes AddressGeocoding.Resolved(coordinate, precision, …) (carrying the
parsed house number); a miss becomes AddressGeocoding.Failed(now). Because the
geocoding service is itself a no-op when no provider is registered,
tier 0 degrades gracefully to Failed rather than throwing.
Tier 1 — deliverability (optional). If — and only if — an
IAddressDeliverabilityService
is registered, the service checks the address against it and maps the provider’s
outcome onto the verification verdict. With no provider, Verification stays
AddressVerification.Unverified and tier 1 is silently skipped.
| Provider outcome | Recorded verdict | Source |
|---|---|---|
Verified | ProviderVerified | VerificationProvider |
Corrected | Corrected | VerificationProvider |
Invalid | Invalid | VerificationProvider |
Unverifiable | Unverified | — |
The provider’s raw match code is carried into the verdict’s Evidence field for
provenance.
[DependsOn(typeof(GranitAddressEnrichmentModule))]public sealed class CrmModule : GranitModule { }GranitAddressEnrichmentModule depends on GranitGeocodingModule, so a geocoding
service is always present (the no-op default when no provider is installed).
IAddressEnrichmentService is registered scoped. Deliverability is wired as a
soft dependency — register a provider to light up tier 1, leave it out to stay at
geocoding plausibility:
public sealed class CustomerWriter(IAddressEnrichmentService enrichment){ public async Task SetAddressAsync(Customer customer, Address address, CancellationToken ct) { AddressEnrichmentResult e = await enrichment.EnrichAsync(address, ct); customer.SetAddress(address, e.Geocoding, e.Verification); }}See also
Section titled “See also”- Address value objects — the
AddressGeocodingandAddressVerificationshapesEnrichAsyncreturns. - Geocoding — tier 0.
- Address deliverability — the tier-1 contract.
- Address platform overview — the three axes and three evidence tiers.