Context Makes the Keyword
Myth: Contextual keywords are always reserved and can't be used as identifiers. Reality: They're only keywords in specific syntactic positions, letting you use words like where or async as variable names in other contexts.
C# uses contextual keywords to add language features without breaking existing code. When C# 5 introduced async and await, codebases with methods named "async" didn't suddenly break. The compiler recognizes these words as special only where they modify syntax.
You'll learn which words are contextual keywords, where they gain special meaning, and how to use them correctly in modern C#. Understanding this distinction helps you read compiler errors and write clearer code.
async and await: Method Modifiers
The async keyword marks a method as asynchronous, enabling the use of await within it. These are contextual because they only have meaning in method signatures and expressions. Outside method declarations, async is just an identifier.
When you mark a method with async, the compiler transforms its body to support awaiting asynchronous operations. The await keyword suspends method execution until the awaited task completes, then resumes from that point.
namespace Services;
public class DataService
{
private readonly HttpClient _httpClient;
public DataService(HttpClient httpClient)
{
_httpClient = httpClient;
}
// 'async' is contextual here - marks method as asynchronous
public async Task<string> GetDataAsync(string url)
{
// 'await' is contextual here - suspends until response arrives
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
// 'async' as a parameter name - allowed outside method modifier position
public void ProcessCallback(Action async)
{
async(); // Valid identifier usage
}
}
In GetDataAsync, async and await are keywords. In ProcessCallback, async is just a parameter name. The compiler distinguishes based on syntactic position. That said, avoid using contextual keywords as identifiers—it confuses readers.
var and dynamic: Type Declarations
The var keyword enables implicit typing where the compiler infers the type from the initializer. It's contextual because it only works in variable declarations with initialization. The type is determined at compile time and never changes.
The dynamic keyword creates variables whose type checking is deferred until runtime. Unlike var, dynamic is a true type that bypasses compile-time type checking. Use it sparingly for COM interop or truly dynamic scenarios.
namespace Examples;
public class TypeExamples
{
public void DemonstrateVar()
{
// 'var' - compiler infers type as string
var message = "Hello";
// message = 42; // Error: cannot convert int to string
// 'var' - compiler infers List<int>
var numbers = new List<int> { 1, 2, 3 };
numbers.Add(4); // Intellisense knows this is List<int>
}
public void DemonstrateDynamic()
{
// 'dynamic' - type checking happens at runtime
dynamic value = "Hello";
Console.WriteLine(value.Length); // Works - string has Length
value = 42;
Console.WriteLine(value.ToString()); // Works - int has ToString()
// This compiles but throws at runtime
// Console.WriteLine(value.NonExistentMethod());
}
public void CombiningBoth()
{
// var for known types (compile-time safety)
var person = new { Name = "Alice", Age = 30 };
// dynamic when interacting with JSON or COM
dynamic json = System.Text.Json.JsonDocument.Parse(
"{\"name\":\"Bob\"}");
string name = json.RootElement.GetProperty("name").GetString();
}
}
Use var for cleaner code when the type is obvious from the right side. Reserve dynamic for situations where you genuinely don't know the type at compile time. Most code should prefer var over dynamic for type safety.
LINQ Query Keywords
LINQ introduces several contextual keywords that form query expressions: from, where, select, join, group, into, orderby, let, ascending, and descending. These only act as keywords inside query expressions.
Query syntax provides a SQL-like way to write LINQ queries. Behind the scenes, the compiler transforms query syntax into method calls. You can use these words as identifiers outside queries without conflict.
namespace Queries;
public class QueryDemo
{
public void QuerySyntaxExample()
{
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 'from', 'where', 'orderby', 'descending', 'select' are contextual
var query = from n in numbers
where n % 2 == 0
orderby n descending
select n * n;
foreach (var result in query)
{
Console.WriteLine(result);
}
}
// Outside queries, these words work as identifiers
public void ProcessData(int where, string select)
{
var from = where + 5; // 'from' as variable name
Console.WriteLine($"{select}: {from}");
}
public void GroupingExample()
{
var people = new[]
{
new { Name = "Alice", Age = 25 },
new { Name = "Bob", Age = 30 },
new { Name = "Carol", Age = 25 }
};
// 'group', 'by', 'into' are contextual here
var grouped = from p in people
group p by p.Age into ageGroup
select new { Age = ageGroup.Key, Count = ageGroup.Count() };
foreach (var item in grouped)
{
Console.WriteLine($"Age {item.Age}: {item.Count} people");
}
}
}
The query keywords only have special meaning inside LINQ expressions. While you can use them as identifiers elsewhere, doing so makes code harder to read. Most style guides recommend avoiding this practice even though it's technically legal.
Property and Accessor Keywords
Keywords like get, set, value, init, add, remove, and partial are contextual to specific declaration contexts. They define property accessors, event accessors, or type modifiers.
The value keyword is special—it represents the incoming value in property setters and init accessors. It's implicitly typed and only exists within those specific contexts.
namespace Models;
public class Person
{
private string _name = string.Empty;
// 'get' and 'set' are contextual keywords in properties
public string Name
{
get => _name;
set // 'value' is contextual keyword representing new value
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
// 'init' is contextual (C# 9+) - allows setting only during initialization
public int Age { get; init; }
// 'partial' is contextual - splits class across files
public partial class Configuration
{
public string Setting1 { get; set; }
}
}
// Separate file could have:
// public partial class Configuration
// {
// public string Setting2 { get; set; }
// }
public class EventDemo
{
private EventHandler? _myEvent;
// 'add' and 'remove' are contextual in event accessors
public event EventHandler MyEvent
{
add { _myEvent += value; }
remove { _myEvent -= value; }
}
// Outside property/event context, these work as identifiers
public void Process(string get, string set, int add)
{
var value = $"{get} {set} {add}";
Console.WriteLine(value);
}
}
These accessor keywords only gain meaning inside their declaration contexts. While technically you can use them as variable names elsewhere, it's poor style. Modern analyzers often warn against this practice.
when and yield: Conditional and Iterator Keywords
The when keyword is contextual in exception filters and pattern matching. It allows conditional catch blocks and pattern guards. The yield keyword marks iterator methods that return values lazily.
Both keywords enable powerful features without requiring full language reservation. This demonstrates how contextual keywords let C# evolve while maintaining compatibility.
namespace Advanced;
public class AdvancedKeywords
{
// 'when' in exception filters
public void HandleException()
{
try
{
RiskyOperation();
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
Console.WriteLine("Timeout occurred - will retry");
}
}
// 'when' in pattern matching
public string ClassifyNumber(int number) => number switch
{
< 0 => "Negative",
0 => "Zero",
> 0 when number % 2 == 0 => "Positive Even",
_ => "Positive Odd"
};
// 'yield' for iterator methods
public IEnumerable<int> GenerateNumbers(int count)
{
for (int i = 0; i < count; i++)
{
yield return i * i; // 'yield' is contextual here
}
}
// Outside iterator context, 'yield' could be an identifier
public void ProcessData(int yield)
{
var when = yield * 2; // Both as variable names (avoid this!)
Console.WriteLine(when);
}
private void RiskyOperation()
{
throw new TimeoutException("Operation timed out");
}
}
The when keyword enables conditional logic in places where traditional if statements won't work. The yield keyword transforms methods into state machines that produce values on demand, enabling efficient lazy evaluation.
Try It Yourself
Build a small program that demonstrates how contextual keywords work in different positions. You'll see how the same word can be a keyword in one context and a regular identifier in another.
Steps
- Scaffold the project:
dotnet new console -n KeywordDemo
- Enter the directory:
cd KeywordDemo
- Replace Program.cs with the code below
- Ensure the .csproj matches the shown configuration
- Launch with
dotnet run
Console.WriteLine("=== Contextual Keywords Demo ===\n");
// var as keyword - type inference
var numbers = new[] { 1, 2, 3, 4, 5 };
// 'where' as LINQ keyword
var evens = from n in numbers
where n % 2 == 0
select n;
Console.WriteLine($"Even numbers: {string.Join(", ", evens)}");
// Using 'where' as a parameter name (valid but confusing)
ProcessData(10, "result");
// Demonstrating yield
var squares = GenerateSquares(5);
Console.WriteLine($"Squares: {string.Join(", ", squares)}");
// Async/await demonstration
await FetchDataAsync();
void ProcessData(int where, string select)
{
// 'where' and 'select' used as identifiers
Console.WriteLine($"\n{select}: value is {where}");
}
IEnumerable<int> GenerateSquares(int count)
{
for (int i = 1; i <= count; i++)
{
yield return i * i; // 'yield' is keyword here
}
}
async Task FetchDataAsync()
{
// 'async' and 'await' as keywords
await Task.Delay(100);
Console.WriteLine("\nAsync operation completed");
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Run result
=== Contextual Keywords Demo ===
Even numbers: 2, 4
result: value is 10
Squares: 1, 4, 9, 16, 25
Async operation completed
Notice how words like 'where', 'select', 'yield', and 'async' act as keywords in specific positions but can theoretically be identifiers elsewhere. While legal, using contextual keywords as identifiers reduces readability.