---
title: "Deduplicate contacts by matching name keys in C#"
url: https://recursiveai.org/onecontact/cookbook/deduplicate-contacts/
description: "Deduplicate contacts by matching name keys in C#: runnable OneContact example with real output, matching rules and practical variations."
version: "1.0.0"
---

# Deduplicate contacts by matching name keys in C#

**OneContact** is a free, open-source contact data library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies, no network access.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneContact) · [GitHub](https://github.com/recursive-ai-oss/onecontact-csharp) · [Docs](https://recursiveai.org/onecontact/docs/) · [Download example (.zip)](https://recursiveai.org/onecontact/cookbook/deduplicate-contacts/deduplicate-contacts.zip)

Group contact names by a normalized comparison key to find candidate duplicates. A shared name key is not proof that two records are the same person.

```shell
dotnet add package RecursiveAI.OneContact
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneContact;
string[] names =
    ["Bob Smith", "Robert Smith"];
var parsed = names.Select(PersonName.Parse);
foreach (var name in parsed
    .DistinctBy(name => name.Key))
    Console.WriteLine(name.Canonical);
```

```text
Bob Smith
```

Unzip [the example](https://recursiveai.org/onecontact/cookbook/deduplicate-contacts/deduplicate-contacts.zip), then `dotnet run`.

## Full program

All sample input is written directly in the program.

```csharp
using RecursiveAI.OneContact;

string[] inputs = ["Bob Smith", "Robert Smith", "Jane Smith"];
var groups = inputs.Select(PersonName.Parse)
    .GroupBy(name => name.Key, StringComparer.Ordinal);
foreach (var group in groups)
{
    Console.WriteLine($"Key: {group.Key}");
    Console.WriteLine($"Keep: {group.First().Canonical}");
    Console.WriteLine($"Matches: {group.Count()}");
}
```

`dotnet run` prints:

```text
Key: robert smith
Keep: Bob Smith
Matches: 2
Key: jane smith
Keep: Jane Smith
Matches: 1
```

The comparison key maps the nickname “Bob” to its formal name “Robert”, which is why both names share `robert smith`; display names stay unchanged.

`PersonName.Parse` throws `ContactParseException` on rejected input; use `TryParse` before grouping a batch that contains untrusted or mixed-kind text.

## Variations

Compare canonical equality with key equality:

```csharp
using RecursiveAI.OneContact;
var first = PersonName.Parse("Bob Smith");
var second = PersonName.Parse("Robert Smith");
Console.WriteLine(first == second);
Console.WriteLine(Contacts.SameKey(first, second));
```

```text
False
True
```

## Related recipes

- [Parse person name](https://recursiveai.org/onecontact/cookbook/parse-person-name/)

## The short answer

Parse each contact’s name with OneContact, then group or deduplicate by `PersonName.Key`. The example groups `Bob Smith` with `Robert Smith`, because the documented key rules map that nickname to its formal name. It keeps `Jane Smith` separate. The snippet at the top retains one name per key; the full program shows the group sizes before any records are discarded.

This page matches names attached to contacts. It does not implement a full contact-record merge policy. Review a candidate group against the fields your application trusts before deleting or combining records. Two different people can have exactly the same name, including exactly the same comparison key.

## How do I remove duplicates from a list of objects in C#?

Choose the property that defines equality for the operation, then use `DistinctBy` for one representative or `GroupBy` to inspect every member. Here that property is the parsed name’s key. The LINQ operation performs grouping; OneContact supplies the name-aware comparison value.

The full program prints the first canonical name in each group and a count. That makes the decision visible: a group of two is a pair of matching names, not two independently verified copies of one person. In a real contact list, retain record identifiers and the original fields while reviewing the group. A key is useful as an index into candidates, not as a substitute for the source record.

Ordinary `PersonName` equality compares canonical forms ordinally. Key equality is explicit. The variation prints `False` for canonical equality and `True` for `Contacts.SameKey`, showing why deduplicating the value objects directly would not produce this example’s result. See the [API notes](https://recursiveai.org/onecontact/docs/api-shapes/) for value and comparison conventions.

## How do I remove duplicates by a property with LINQ?

Use the comparison key property, not a lowercased copy of the display name. OneContact’s [person key rules](https://recursiveai.org/onecontact/docs/name-keys/) explain the distinction: the key folds accents, removes punctuation, maps selected nicknames, reduces middle names to initials and combines family-name parts. Titles and suffixes are dropped.

Those transformations are intentionally lossy. `Canonical` stays faithful enough for display; `Key` sacrifices distinctions to bring likely variants together. Never write the key back into the contact’s name field. Keep both when your workflow needs display and lookup.

The published key version’s output is stable; a rule change ships as a new `KeyVersion`. If keys are persisted in a database, preserve which version generated them. Comparing values generated under different rule versions without an explicit migration can undermine the meaning of the index.

## Should I use DistinctBy or GroupBy in C#?

Use `DistinctBy` when keeping one representative per key is already the agreed policy. Use `GroupBy` when the members need inspection or when the output must explain why records were proposed as duplicates. The latter is the safer starting point for a contact-cleanup screen because no member disappears before review.

Name matching and whole-record merging are different operations. Even the library’s extraction merge rules are contextual: the [extraction notes](https://recursiveai.org/onecontact/docs/extraction/) say phone numbers never decide a record merge because a main line can be shared. Don’t extend a name-only example into an unconditional rule that matching phone numbers or similar spellings identify one person. Make that decision against the requirements of the data you actually hold.
