Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration!How!?

Querying

IContactStore.Query() returns a ContactQuery — a small fluent builder. Nothing runs until you call a terminal method (ToListAsync, FirstOrDefaultAsync, CountAsync), and the native read is performed off the calling thread, so it is safe to await straight from the UI thread.

var results = await contactStore
.Query()
.Where(ContactField.FamilyName, "Smith", ContactFilterOperation.StartsWith)
.OrderBy(ContactSortField.FamilyName)
.ThenBy(ContactSortField.GivenName)
.Take(50)
.ToListAsync(ct);

Field filters are pushed down to the native query where the platform supports it — ContentProvider selections on Android, a CNContact fetch predicate on iOS — and are always re-applied in-memory, so the result is exact no matter how much the platform could translate.

// default operation is Contains
var johns = await contactStore.Query()
.Where(ContactField.GivenName, "John")
.ToListAsync();
// by phone number — matches if ANY of the contact's numbers match
var byPhone = await contactStore.Query()
.Where(ContactField.Phone, "555")
.ToListAsync();
// by email
var byEmail = await contactStore.Query()
.Where(ContactField.Email, "@example.com")
.ToListAsync();

Multiple field filters are ANDed by default:

var results = await contactStore.Query()
.Where(ContactField.GivenName, "J", ContactFilterOperation.StartsWith)
.Where(ContactField.FamilyName, "Smith")
.ToListAsync();

Call Match(ContactFilterMatch.Any) to OR them instead.

Search(text) is the “one text box over everything” filter — it matches given name, family name, display name, phone numbers and email addresses, and switches the query to Match.Any:

var results = await contactStore.Query()
.Search(searchText)
.OrderBy(ContactSortField.FamilyName)
.ToListAsync();

Pass a plain predicate for anything the fields don’t cover. It runs in-memory after the fetch, and multiple predicates are ANDed:

var birthdays = await contactStore.Query()
.Where(c => c.Dates.Any(d => d.Type == ContactDateType.Birthday))
.ToListAsync();
var page = await contactStore.Query()
.Where(ContactField.FamilyName, "A", ContactFilterOperation.StartsWith)
.OrderBy(ContactSortField.FamilyName)
.ThenBy(ContactSortField.GivenName)
.Skip(10)
.Take(20)
.ToListAsync();

OrderBy replaces any previous sort; ThenBy adds a secondary one and throws if no OrderBy precedes it. Both take a descending flag. Sorting and paging are applied after the fetch.

Method Returns
ToListAsync(ct) IReadOnlyList<Contact>
FirstOrDefaultAsync(ct) Contact?
CountAsync(ct) int

ContactField: GivenName, FamilyName, MiddleName, NamePrefix, NameSuffix, Nickname, DisplayName, Note, Company, JobTitle, Department, Phone, Email.

ContactFilterOperation: Contains (default), StartsWith, EndsWith, Equals. All comparisons are case-insensitive.

Field Android iOS
Name fields (GivenName, FamilyName, MiddleName, NamePrefix, NameSuffix, DisplayName, Nickname) ✅ selection query ✅ for StartsWith/Equals on given/family/display name
Phone, Email ✅ selection query
Note, Company, JobTitle, Department
Predicate Where(Func<Contact,bool>)

❌ means the platform reads the full contact list and the builder filters it in-memory — correct, just more work. CNContact’s only search predicate matches on a name-token prefix, which is why iOS pushes down StartsWith/Equals but not Contains/EndsWith, and never in Match.Any mode.

Returns a sorted, distinct list of uppercase first letters from all contacts’ family names:

var letters = await contactStore.GetFamilyNameFirstLetters();
// ['A', 'B', 'C', 'D', ...]

This is useful for building alphabetical index/jump lists in contact list UIs.