Dictionaries
These methods cover the dictionary code you write over and over: read a key that may be missing, create a value on first use, count things, remove entries by a condition, combine two dictionaries, and turn a dictionary into an object or a URL query string. Reach for them when you would otherwise write a TryGetValue / if / assign block by hand.
The methods live in the System.Collections.Generic namespace, so you do not need an extra using directive. The exception is the ToQueryString for a NameValueCollection or string pairs, which lives in System.Collections.Specialized (see below).
At a glance
| Method | What it does |
|---|---|
GetOrDefault(key) | Returns the value, or default when the key is missing. |
GetOrDefault(key, defaultValue) | Returns the value, or the fallback you pass when the key is missing. |
TryGetValue<T> | Reads a value from an IDictionary<string, object?> and checks its type in one step. |
GetOrAdd | Returns the value, or creates it with a factory, stores it and returns it. |
Increment | Adds 1 (or an amount) to an int counter; a missing key starts at 0. |
RemoveWhere | Removes every entry that matches a (key, value) predicate and returns the count. |
AddOrUpdate | Stores a value for a new key, or replaces it with a computed value for an existing key. |
Merge | Combines two dictionaries into a new one; the second wins, or a resolver decides. |
Invert | Swaps keys and values into a new dictionary. |
ToObject<T> | Creates a T and copies each entry to the property with the same name, converting types. |
ToQueryString (dictionary) | Builds an encoded query string from an IDictionary<string, object?>. |
ToQueryString (NameValueCollection and pairs) | Builds an encoded query string from a NameValueCollection or string pairs. |
Two related features are documented on other pages: the defaults + overrides operator (a C# 14 extension member that calls Merge) on Extension members, and the typo-tolerant GetClosest / GetClosestKey lookups on Fuzzy and natural matching.
Common rules
- A
nulldictionary,nullkey ornulldelegate throwsArgumentNullExceptionwith the parameter name (source,key,factory,predicate, ...). - Methods that return a dictionary (
Merge,Invert) return a newDictionary<TKey, TValue>. They never change their inputs. - Methods that change a dictionary (
GetOrAdd,Increment,RemoveWhere,AddOrUpdate) change the one you call them on. - Formatting (
ToQueryString) and conversion (ToObject<T>) use the invariant culture.
See Conventions for the rules that apply across the whole library.
Reading values
GetOrDefault
GetOrDefault(key) returns the stored value, or default(TValue) when the key is missing: null for reference types, 0 for numbers. It works on Dictionary<TKey, TValue>, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> and ConcurrentDictionary<TKey, TValue>.
Dictionary<string, string> statusByOrder = new Dictionary<string, string>
{
["ORD-1001"] = "Shipped",
};
string? shipped = statusByOrder.GetOrDefault("ORD-1001"); // → "Shipped"
string? unknown = statusByOrder.GetOrDefault("ORD-9999"); // → null
IReadOnlyDictionary<string, int> stockBySku = new Dictionary<string, int> { ["SKU-1"] = 12 };
int inStock = stockBySku.GetOrDefault("SKU-1"); // → 12
int missing = stockBySku.GetOrDefault("SKU-2"); // → 0
For Dictionary<string, int>, a missing key and a key stored with 0 both return 0. When you need to tell them apart, use TryGetValue, or pass an explicit fallback with GetOrDefault(key, defaultValue).
GetOrDefault with a fallback value
GetOrDefault(key, defaultValue) returns the stored value, or defaultValue when the key is missing. A stored value always wins, even when it equals default. It has the same four receivers as GetOrDefault(key).
Dictionary<string, string> userSettings = new Dictionary<string, string>
{
["language"] = "ar",
};
string language = userSettings.GetOrDefault("language", "en"); // → "ar"
string theme = userSettings.GetOrDefault("theme", "light"); // → "light"
ConcurrentDictionary<string, int> retryLimits = new ConcurrentDictionary<string, int>();
retryLimits["payments"] = 1;
int paymentRetries = retryLimits.GetOrDefault("payments", 5); // → 1
int emailRetries = retryLimits.GetOrDefault("email", 5); // → 5
Dictionary and ConcurrentDictionary have their own overloads. A type that implements both IDictionary<TKey, TValue> and IReadOnlyDictionary<TKey, TValue> but has no overload of its own, such as SortedDictionary<TKey, TValue>, matches two overloads equally well, so the call does not compile. Store it in a variable typed as one of the interfaces, or cast it: ((IReadOnlyDictionary<string, int>)sorted).GetOrDefault("a", 0).
TryGetValue<T>
TryGetValue<T>(key, out T value) reads a value from an IDictionary<string, object?> and checks that it is a T. It returns true only when the key exists and the value is a T. Use it for property bags, JSON-like payloads and context dictionaries where values are stored as object.
IDictionary<string, object?> context = new Dictionary<string, object?>
{
["tenantId"] = 42,
["retryCount"] = "3",
["traceId"] = null,
};
bool hasTenant = context.TryGetValue("tenantId", out int tenantId); // → true (tenantId → 42)
bool hasRetries = context.TryGetValue("retryCount", out int retryCount); // → false (retryCount → 0)
bool hasTrace = context.TryGetValue("traceId", out string? traceId); // → false (traceId → null)
bool hasUser = context.TryGetValue("userId", out string? userId); // → false (userId → null)
- The check is a type test (
value is T), not a conversion. The string"3"is not anint, so the call returnsfalse. UseToObject<T>when you need conversion. - A stored
nullreturnsfalse, becausenullis not aT. - The out variable must have an explicit type other than
object?. Without object? valuethe call binds to the built-inIDictionary.TryGetValue.
Changing a dictionary in place
GetOrAdd
GetOrAdd(key, factory) returns the stored value. When the key is missing, it calls factory, stores the result and returns it. The factory runs only for a missing key. There are three overloads:
| Receiver | Factory |
|---|---|
IDictionary<TKey, TValue> | Func<TKey, TValue> (receives the key) |
IDictionary<TKey, TValue> | Func<TValue> |
ConcurrentDictionary<TKey, TValue> | Func<TValue> |
Dictionary<string, List<string>> invoicesByCustomer = new Dictionary<string, List<string>>();
invoicesByCustomer.GetOrAdd("CUST-7", () => new List<string>()).Add("INV-100");
invoicesByCustomer.GetOrAdd("CUST-7", () => new List<string>()).Add("INV-101");
int invoiceCount = invoicesByCustomer["CUST-7"].Count; // → 2
IDictionary<string, int> lengthByCode = new Dictionary<string, int>();
int created = lengthByCode.GetOrAdd("DISCOUNT", code => code.Length); // → 8
int existing = lengthByCode.GetOrAdd("DISCOUNT", code => 0); // → 8 (factory not called)
The ConcurrentDictionary overload is thread-safe. Under contention the factory may run more than once, but only one value is stored and every caller gets that value. A key-taking factory (k => ...) on a ConcurrentDictionary calls the built-in ConcurrentDictionary.GetOrAdd, which behaves the same way.
For IList<T>.GetOrAdd(selector, factory) see Lists and collections.
Increment
Increment(key) adds 1 to the int stored under key and returns the new count. Increment(key, by) adds by, which may be negative. A missing key starts at 0.
Dictionary<string, int> hitsByPath = new Dictionary<string, int>();
int first = hitsByPath.Increment("/checkout"); // → 1
int second = hitsByPath.Increment("/checkout"); // → 2
Dictionary<string, int> stockBySku = new Dictionary<string, int> { ["SKU-1"] = 10 };
int afterSale = stockBySku.Increment("SKU-1", -4); // → 6
int restocked = stockBySku.Increment("SKU-2", 3); // → 3
int only, and not thread-safeThe receiver is IDictionary<TKey, int>, so a Dictionary<string, long> has no Increment. The read and the write are two separate steps, so Increment is not thread-safe, even on a ConcurrentDictionary. For concurrent counters use ConcurrentDictionary.AddOrUpdate(key, 1, (_, count) => count + 1).
RemoveWhere
RemoveWhere(predicate) removes every entry for which predicate(key, value) returns true and returns how many it removed. It collects the matching keys first, so it does not change the dictionary while enumerating it.
DateTimeOffset now = new DateTimeOffset(2026, 11, 4, 12, 0, 0, TimeSpan.Zero);
Dictionary<string, DateTimeOffset> sessionExpiry = new Dictionary<string, DateTimeOffset>
{
["sess-a"] = now.AddMinutes(-5),
["sess-b"] = now.AddMinutes(30),
["sess-c"] = now.AddHours(-1),
};
int removed = sessionExpiry.RemoveWhere((sessionId, expiresAt) => expiresAt < now); // → 2
IEnumerable<string> remaining = sessionExpiry.Keys; // → ["sess-b"]
The predicate can use the key alone, for example (key, _) => key.StartsWith("tmp-", StringComparison.Ordinal). An empty dictionary, or a predicate that matches nothing, returns 0.
AddOrUpdate
AddOrUpdate(key, addValue, updateValueFactory) stores addValue when the key is missing. When the key exists, it stores updateValueFactory(key, existingValue). It returns the value now stored. This is the ConcurrentDictionary.AddOrUpdate contract, for any IDictionary<TKey, TValue>.
Dictionary<string, decimal> totalByInvoice = new Dictionary<string, decimal>();
decimal firstLine = totalByInvoice.AddOrUpdate("INV-100", 25.00m, (invoiceId, total) => total + 25.00m);
// → 25.00 (new key: addValue stored)
decimal secondLine = totalByInvoice.AddOrUpdate("INV-100", 10.50m, (invoiceId, total) => total + 10.50m);
// → 35.50 (existing key: the factory computes the new value)
- The factory runs only when the key exists;
addValueis ignored in that case. - On a
ConcurrentDictionaryvariable, the same call binds to the built-in, thread-safeConcurrentDictionary.AddOrUpdate.
Combining dictionaries
Merge
Merge(other) returns a new dictionary with every entry from both inputs. When a key exists in both, the value from other wins. That is the usual "defaults, then overrides" pattern for settings.
Merge(other, resolveConflict) lets you decide. For a key in both, it calls resolveConflict(key, existingValue, incomingValue) and keeps the result. The resolver runs only for keys that exist in both.
Dictionary<string, string> defaults = new Dictionary<string, string>
{
["theme"] = "light",
["language"] = "en",
};
Dictionary<string, string> userSettings = new Dictionary<string, string>
{
["theme"] = "dark",
["font"] = "mono",
};
Dictionary<string, string> effective = defaults.Merge(userSettings);
// → { ["theme"] = "dark", ["language"] = "en", ["font"] = "mono" }
Dictionary<string, int> warehouseA = new Dictionary<string, int> { ["apple"] = 3, ["pear"] = 1 };
Dictionary<string, int> warehouseB = new Dictionary<string, int> { ["apple"] = 4, ["plum"] = 2 };
Dictionary<string, int> total = warehouseA.Merge(warehouseB, (sku, existing, incoming) => existing + incoming);
// → { ["apple"] = 7, ["pear"] = 1, ["plum"] = 2 }
- Both inputs are
IEnumerable<KeyValuePair<TKey, TValue>>, so you can merge any dictionary type or a list of pairs. - Neither input is changed. Two empty inputs give an empty dictionary.
- The result uses the default key comparer. A case-insensitive source dictionary does not pass its comparer on, so
"Theme"and"theme"become two keys. - With C# 14 you can write
defaults + overridesinstead ofdefaults.Merge(overrides). See Extension members.
Invert
Invert() swaps keys and values: { "JO": "Jordan" } becomes { "Jordan": "JO" }. Use it to build a reverse lookup from a code table.
Dictionary<string, string> countryByCode = new Dictionary<string, string>
{
["JO"] = "Jordan",
["EG"] = "Egypt",
};
Dictionary<string, string> codeByCountry = countryByCode.Invert();
string egyptCode = codeByCountry["Egypt"]; // → "EG"
Values become keys, so two keys with the same value, or a null value, throw ArgumentException (ParamName "source"). An empty input returns an empty dictionary.
Converting dictionaries
ToObject<T>
ToObject<T>() creates a T with its parameterless constructor and copies each entry of an IDictionary<string, object?> to the public property with the same name, if it has a setter. Values are converted to the property type when needed: "42" to int, "Monday" to DayOfWeek, "7" to int?, a GUID string to Guid. Use it to map form fields, configuration rows or loosely typed payloads to a class.
public sealed class CustomerForm
{
public string? Name { get; set; }
public int Age { get; set; }
public DayOfWeek PreferredDeliveryDay { get; set; }
public int? LoyaltyPoints { get; set; }
}
IDictionary<string, object?> fields = new Dictionary<string, object?>
{
["Name"] = "Lina",
["Age"] = "42",
["PreferredDeliveryDay"] = "Monday",
["LoyaltyPoints"] = null,
["CsrfToken"] = "abc123",
};
CustomerForm form = fields.ToObject<CustomerForm>();
// form.Name → "Lina"
// form.Age → 42
// form.PreferredDeliveryDay → DayOfWeek.Monday
// form.LoyaltyPoints → null
- Keys match property names exactly and case-sensitively:
"age"does not setAge. - Keys with no matching public property (
"CsrfToken"above), or whose property has no setter, are ignored. A property with aprivateorinitsetter is still set. An empty dictionary gives aTwith default property values. - Text is parsed with the invariant culture.
- A value that cannot be converted throws
InvalidCastException, for examplenullfor anintproperty or an arbitrary object for anint. Text in the wrong format, such as"abc"for anint, throwsFormatException.
T is annotated with [DynamicallyAccessedMembers(PublicProperties)], so the trimmer keeps the properties that ToObject<T> sets. See Trimming and AOT.
ToQueryString on a dictionary
ToQueryString() on an IDictionary<string, object?> builds a URL query string, without a leading ?. Keys and values are percent-encoded (a space becomes %20, & becomes %26). Values are formatted the same way on every machine:
| Value | Written as |
|---|---|
number, other IFormattable | invariant culture: 1.5, 1000 |
bool | true / false |
| enum | its name: Friday |
DateTime, DateTimeOffset | ISO 8601 round-trip format (o) |
DateOnly | yyyy-MM-dd |
TimeOnly | HH:mm:ss, plus a fraction only when there is one |
null | an empty value: key= |
| collection | the key repeated per item; null items are skipped |
Dictionary<string, object?> query = new Dictionary<string, object?>
{
["q"] = "red shoes & bags",
["minPrice"] = 19.5m,
["inStock"] = true,
["deliveryDay"] = DayOfWeek.Friday,
["coupon"] = null,
["ids"] = new int?[] { 1, null, 2 },
};
string queryString = query.ToQueryString();
q=red%20shoes%20%26%20bags&minPrice=19.5&inStock=true&deliveryDay=Friday&coupon=&ids=1&ids=2
Dictionary<string, object?> range = new Dictionary<string, object?>
{
["from"] = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc),
["day"] = new DateOnly(2026, 11, 4),
["at"] = new TimeOnly(14, 30),
};
string queryString = range.ToQueryString();
// → "from=2024-01-02T03%3A04%3A05.0000000Z&day=2026-11-04&at=14%3A30%3A00"
- An empty dictionary returns
"". - Numbers ignore the current culture: under
de-DE,1.5mis still written as1.5. - A value that is not
IFormattableis written with its ownToString(), which may depend on the current culture. - Strings inside a collection are values, not nested collections:
["tags"] = new List<string> { "a b", "c" }givestags=a%20b&tags=c. - A collection inside a collection value (for example a
List<int[]>) throwsArgumentException(ParamName"source") whose message names the key, because a query string cannot express it. - A
byte[]is a collection, so each byte repeats the key.
This method is for flat values. For an object with nested properties, object.ToQueryString() writes Address.City=.... See Objects.
ToQueryString on a NameValueCollection or string pairs
NameValueCollection.ToQueryString() builds a query string and repeats a key that has several values. A second overload takes any IEnumerable<KeyValuePair<string, string?>>, such as a Dictionary<string, string?> or a list of pairs with repeated keys. Both percent-encode keys and values (a space becomes %20, not +) and write a null value as key=.
NameValueCollection filters = new NameValueCollection
{
{ "status", "open" },
{ "status", "pending" },
{ "assigned to", "a&b" },
};
string queryString = filters.ToQueryString();
// → "status=open&status=pending&assigned%20to=a%26b"
using System.Collections.Specialized;
Dictionary<string, string?> search = new Dictionary<string, string?>
{
["name"] = "John Doe",
["company"] = null,
["q"] = "a+b",
};
string fromDictionary = search.ToQueryString();
// → "name=John%20Doe&company=&q=a%2Bb"
List<KeyValuePair<string, string?>> ids = new List<KeyValuePair<string, string?>>
{
new KeyValuePair<string, string?>("id", "1"),
new KeyValuePair<string, string?>("id", "2"),
};
string fromPairs = ids.ToQueryString();
// → "id=1&id=2"
- An empty collection returns
"". - The values are already strings, so no number or date formatting happens. Use the dictionary overload when your values are numbers, dates or collections.
using System.Collections.Specialized;Both overloads live in System.Collections.Specialized, which implicit usings do not import. The using you add for NameValueCollection also brings them into scope, but for a Dictionary<string, string?> or a list of pairs you must add it yourself. Without it, search.ToQueryString() still compiles: it binds to object.ToQueryString() and writes the dictionary's own properties (Count=3&Capacity=3&Keys=name&...).
See also
- Lists and collections —
IList<T>.GetOrAddand other collection helpers. - Extension members — the
defaults + overridesmerge operator. - Fuzzy and natural matching —
GetClosestandGetClosestKeyfor "did you mean?" lookups. - Objects —
object.ToQueryString()andAsDictionaryfor whole objects.