Learn how to add custom request headers and read response headers in Microsoft Graph API requests
Custom headers allow you to control request behavior, handle conditional requests, and access metadata returned by the Microsoft Graph API. The SDK provides easy access to add request headers and read response headers.
var user = await graphClient.Me.GetAsync(config =>{ config.Headers.Add("Prefer", "return=minimal"); config.Headers.Add("ConsistencyLevel", "eventual"); config.Headers.Add("Cache-Control", "no-cache");});
Headers are added to the config.Headers collection within the request configuration lambda. The SDK automatically includes authentication and other required headers.
Conditional requests for optimistic concurrency control.
// Update only if resource hasn't changed (using ETag)var etag = "W/\"abc123\""; // ETag from previous GETtry{ var updatedUser = await graphClient.Me.PatchAsync(userUpdate, config => { config.Headers.Add("If-Match", etag); });}catch (ODataError ex) when (ex.ResponseStatusCode == 412){ Console.WriteLine("Resource was modified by another request"); // Fetch latest version and retry}// Get resource only if it has changedvar user = await graphClient.Me.GetAsync(config =>{ config.Headers.Add("If-None-Match", etag);});// Returns 304 Not Modified if unchanged
// Most SDK methods return strongly-typed objects// For response headers, you may need to use lower-level APIs// Example: Get message with ETag from responsevar message = await graphClient.Me.Messages["id"].GetAsync();// ETag is typically available in AdditionalDataif (message.AdditionalData?.TryGetValue("@odata.etag", out var etagValue) == true){ var etag = etagValue.ToString(); Console.WriteLine($"ETag: {etag}");}
var user = await graphClient.Me.GetAsync();// ETag often available in OData metadataif (user.AdditionalData?.TryGetValue("@odata.etag", out var etagObj) == true){ var etag = etagObj.ToString(); // Use ETag for conditional update var update = new User { JobTitle = "Senior Developer" }; try { await graphClient.Me.PatchAsync(update, config => { config.Headers.Add("If-Match", etag); }); } catch (ODataError ex) when (ex.ResponseStatusCode == 412) { Console.WriteLine("Precondition failed - resource was modified"); }}
try{ var users = await graphClient.Users.GetAsync();}catch (ODataError ex) when (ex.ResponseStatusCode == 429){ // Check for Retry-After in error details if (ex.Error?.InnerError?.AdditionalData?.TryGetValue("Retry-After", out var retryAfter) == true) { var seconds = int.Parse(retryAfter.ToString()); Console.WriteLine($"Throttled. Retry after {seconds} seconds"); await Task.Delay(TimeSpan.FromSeconds(seconds)); // Retry request }}
Returned after POST requests, contains URL of created resource.
var newGroup = new Group{ DisplayName = "New Team", MailEnabled = false, MailNickname = "newteam", SecurityEnabled = true};var group = await graphClient.Groups.PostAsync(newGroup);// The created resource URL is typically in the id propertyConsole.WriteLine($"Created group: {group.Id}");Console.WriteLine($"URL: https://graph.microsoft.com/v1.0/groups/{group.Id}");
Only add ConsistencyLevel: eventual when required for specific operations:
// Required - contains filterconfig.Headers.Add("ConsistencyLevel", "eventual");// Not required - simple filterconfig.QueryParameters.Filter = "department eq 'Engineering'";// Don't add ConsistencyLevel here
Implement ETag-based updates for critical data
Use conditional updates to prevent lost updates:
// Good - uses ETag to prevent conflictsvar user = await graphClient.Me.GetAsync();var etag = GetETag(user);await graphClient.Me.PatchAsync(updates, config => config.Headers.Add("If-Match", etag));// Bad - may overwrite concurrent changesawait graphClient.Me.PatchAsync(updates);
Handle Retry-After header when throttled
Always respect the Retry-After header:
catch (ODataError ex) when (ex.ResponseStatusCode == 429){ var retryAfter = GetRetryAfter(ex); await Task.Delay(TimeSpan.FromSeconds(retryAfter)); // Retry}
Use Prefer header for response optimization
Reduce payload size with appropriate Prefer values:
// Return minimal response after updateconfig.Headers.Add("Prefer", "return=minimal");// Only when you need the full updated objectconfig.Headers.Add("Prefer", "return=representation");
public async Task<T> UpdateWithConcurrencyCheckAsync<T>( Func<Task<T>> getResource, Func<string, T, Task<T>> updateResource, T updates) where T : class{ int maxRetries = 3; for (int i = 0; i < maxRetries; i++) { // Get current version var current = await getResource(); // Extract ETag string etag = null; if (current is IParsable parsable) { parsable.AdditionalData?.TryGetValue("@odata.etag", out var etagObj); etag = etagObj?.ToString(); } if (string.IsNullOrEmpty(etag)) { throw new InvalidOperationException("ETag not available"); } try { // Try update with ETag return await updateResource(etag, updates); } catch (ODataError ex) when (ex.ResponseStatusCode == 412 && i < maxRetries - 1) { Console.WriteLine($"Conflict detected. Retry {i + 1}/{maxRetries}"); await Task.Delay(TimeSpan.FromMilliseconds(500 * (i + 1))); } } throw new InvalidOperationException("Failed to update after max retries");}// Usageawait UpdateWithConcurrencyCheckAsync( () => graphClient.Me.GetAsync(), (etag, updates) => graphClient.Me.PatchAsync(updates, config => config.Headers.Add("If-Match", etag)), new User { JobTitle = "Senior Developer" });