Microsoft Graph from Zero to Production (End-to-End, Exhaustive)
Microsoft Graph is Microsoft 365’s unified REST API surface: one endpoint, many workloads (Entra ID, Outlook, Teams, SharePoint/OneDrive, etc.). If you want to build production-grade integrations, you need to understand not only “how to call an endpoint”, but also auth, consent, least privilege, query shaping, batching, delta sync, webhooks, throttling, and operations. (Microsoft Learn)
1) Mental model: endpoint, resources, versions
1.1 The endpoint + resources
Most calls look like:
https://graph.microsoft.com/v1.0/...for stable, generally available APIshttps://graph.microsoft.com/beta/...for preview APIs that can break without notice (Microsoft Learn)
Graph is “resource-centric”: you navigate nouns (users, groups, sites, drives) and their relationships. (Microsoft Learn)
1.2 v1.0 vs beta (production rule)
Use v1.0 for production. Use beta only for testing/experimentation because breaking changes can happen. (Microsoft Learn)
2) Authentication end-to-end: tokens, scopes, delegated vs app-only
Graph uses OAuth 2.0 access tokens. Your architecture choices start here:
2.1 Delegated access (user context)
- App acts on behalf of a signed-in user
- Effective access is bounded by the user’s own permissions (Microsoft Learn)
2.2 Application (app-only) access
- App acts as itself (daemon, service, scheduled job)
- Requires application permissions and usually admin consent (Microsoft Learn)
2.3 The .default scope (critical concept)
When you request a token for Graph using client credentials, you typically use:
scope = https://graph.microsoft.com/.default
.default means “issue a token containing the application permissions that were granted (consented) on the app registration for that resource.” It does not mean “all permissions”; it means “whatever is configured + consented.” (Microsoft Learn)
3) Permissions & consent: how people get it wrong
3.1 Permissions types
- Delegated permissions (scopes): apply when a user is signed in (Microsoft Learn)
- Application permissions (app roles): apply when the app runs without a user (Microsoft Learn)
3.2 Consent flow
Even if you configure permissions in the app registration, Graph won’t allow calls until permissions are consented (user consent or admin consent depending on privilege). (Microsoft Learn)
3.3 Permissions reference (bookmark this)
The official permissions catalog is your ground truth for what each permission does and whether it’s delegated/application. (Microsoft Learn)
4) Least privilege for SharePoint/OneDrive: “Selected” permissions and Sites.Selected
This is the modern “granular access” story.
4.1 The big idea: consent is not enough
With Selected permissions, an admin can consent the scope, but the app still has zero access until you explicitly assign access to the specific resource (site/list/etc.). (Microsoft Learn)
4.2 Sites.Selected (SharePoint/OneDrive)
Sites.Selectedis designed to avoid tenant-wide “read everything” permissions.- After consenting
Sites.Selected, you must grant the app access to a specific site (explicit assignment). (Microsoft Learn)
Important operational detail: to grant those site-specific permissions, you typically need a tenant admin role or a separate “manager app” with broad rights (commonly used pattern). (GitHub)
4.3 List-level selected permissions (even more granular)
Graph also supports selected scopes down to the list level (e.g., “Lists.SelectedOperations.Selected”), where you must grant list permissions via Graph permissions endpoints. (Microsoft Learn)
5) Calling Graph like a pro: query shaping, filtering, paging
5.1 Query parameters (performance + cost control)
Use OData query parameters to reduce payload and roundtrips:
$selectto choose properties$filterto narrow results$topto page$expandto include related data when supported (Microsoft Learn)
Some directory objects require advanced query capabilities for certain filters/operators. (Microsoft Learn)
6) Batching: fewer roundtrips, but not “free”
Graph supports JSON batching via POST /$batch:
- Up to 20 requests per batch payload (Microsoft Learn)
Key production reality:
- Each sub-request can be throttled independently; you must inspect each sub-response. (Microsoft Learn)
7) Delta queries: efficient sync without full crawls
Delta query is Graph’s built-in “change tracking” pattern:
- Get initial data set
- Continue calling the delta endpoint until you receive a
deltaLink - Store the
deltaLinkand call it later to retrieve only changes (Microsoft Learn)
This is the backbone of scalable “sync to local cache” implementations. (Microsoft Learn)
8) Webhooks: change notifications, lifecycle, rich payloads
Graph can push change notifications through webhooks (and also via Event Grid/Event Hubs in some scenarios). (Microsoft Learn)
Core docs:
- Change notifications overview (Microsoft Learn)
- Receive notifications through webhooks (Microsoft Learn)
Production must-haves:
- Lifecycle notifications to avoid missing renewals / broken flows (Microsoft Learn)
- Rich notifications (resource data) when you need fewer follow-up calls (Microsoft Learn)
9) Reliability engineering: throttling, retries, and backoff
Graph can throttle at any time (429). Your app must be designed for it.
9.1 Rules of survival
- Always handle 429 and honor
Retry-After - Use retries with backoff (and ideally jitter)
- Reduce request burstiness; shape queries; use caching (Microsoft Learn)
Service-specific limits exist and differ by workload. (Microsoft Learn)
10) Minimal production-ready C# pattern (client credentials + raw HTTP)
Below is a complete console-style example that:
- Gets an app-only token (
.default) - Calls Graph with retries (429)
- Demonstrates query shaping
Replace placeholders with your own values. (Use neutral names like
contoso.onmicrosoft.comin documentation.) (Microsoft Learn)
Step 1 — appsettings.json
{
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "11111111-1111-1111-1111-111111111111",
"ClientSecret": "YOUR-SECRET",
"GraphBaseUrl": "https://graph.microsoft.com/v1.0"
}
Step 2 — Program.cs (full code)
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
internal sealed class AppConfig
{
public string TenantId { get; set; } = "";
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
public string GraphBaseUrl { get; set; } = "https://graph.microsoft.com/v1.0";
}
internal static class Program
{
private static readonly string[] GraphScopes = new[] { "https://graph.microsoft.com/.default" };
public static async Task<int> Main()
{
try
{
var config = LoadConfig("appsettings.json");
Validate(config);
var token = await AcquireAppOnlyTokenAsync(config);
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Example 1: Basic call (organization)
var orgJson = await SendWithRetryAsync(http, HttpMethod.Get,
$"{config.GraphBaseUrl}/organization?$select=id,displayName");
Console.WriteLine("=== organization ===");
Console.WriteLine(orgJson);
Console.WriteLine();
// Example 2: Users with query shaping
// Note: $filter support depends on tenant/data and sometimes advanced query capabilities.
var usersJson = await SendWithRetryAsync(http, HttpMethod.Get,
$"{config.GraphBaseUrl}/users?$top=5&$select=id,displayName,userPrincipalName");
Console.WriteLine("=== users (top 5) ===");
Console.WriteLine(usersJson);
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
private static AppConfig LoadConfig(string path)
{
var json = File.ReadAllText(path);
var cfg = JsonSerializer.Deserialize<AppConfig>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return cfg ?? throw new InvalidOperationException("Invalid configuration.");
}
private static void Validate(AppConfig cfg)
{
if (string.IsNullOrWhiteSpace(cfg.TenantId)) throw new ArgumentException("TenantId is required.");
if (string.IsNullOrWhiteSpace(cfg.ClientId)) throw new ArgumentException("ClientId is required.");
if (string.IsNullOrWhiteSpace(cfg.ClientSecret)) throw new ArgumentException("ClientSecret is required.");
if (string.IsNullOrWhiteSpace(cfg.GraphBaseUrl)) throw new ArgumentException("GraphBaseUrl is required.");
}
private static async Task<string> AcquireAppOnlyTokenAsync(AppConfig cfg)
{
var app = ConfidentialClientApplicationBuilder
.Create(cfg.ClientId)
.WithClientSecret(cfg.ClientSecret)
.WithAuthority($"https://login.microsoftonline.com/{cfg.TenantId}")
.Build();
var result = await app.AcquireTokenForClient(GraphScopes).ExecuteAsync();
return result.AccessToken;
}
private static async Task<string> SendWithRetryAsync(HttpClient http, HttpMethod method, string url)
{
const int maxAttempts = 6;
var attempt = 0;
while (true)
{
attempt++;
using (var req = new HttpRequestMessage(method, url))
using (var resp = await http.SendAsync(req))
{
if (resp.StatusCode == (HttpStatusCode)429)
{
if (attempt >= maxAttempts)
throw new HttpRequestException("Too many throttled attempts (429).");
// Respect Retry-After if provided; otherwise use a safe default backoff.
var delaySeconds = 30;
if (resp.Headers.RetryAfter?.Delta != null)
delaySeconds = (int)Math.Ceiling(resp.Headers.RetryAfter.Delta.Value.TotalSeconds);
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
continue;
}
// Consider transient handling for 503/504 similarly in real production code.
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new HttpRequestException($"Graph call failed: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
return body;
}
}
}
}
Why this pattern matters
.defaultfor client credentials is the official identity-platform approach. (Microsoft Learn)- Handling 429 with
Retry-Afteris mandatory for production reliability. (Microsoft Learn) - Query shaping is a first-class performance technique. (Microsoft Learn)
11) Production patterns checklist
Security
- Prefer certificate auth for high-assurance workloads (secrets rotate more often; both are valid patterns depending on your environment).
- Minimize Graph permissions; prefer Selected scopes when feasible. (Microsoft Learn)
Performance
- Use
$selecteverywhere you can. - Batch carefully (up to 20), but inspect sub-responses and plan for per-request throttling. (Microsoft Learn)
- Use delta queries for sync. (Microsoft Learn)
Operations
- Log request IDs + status codes.
- Build dashboards for 429 frequency.
- Keep an upgrade plan (avoid beta dependencies). (Microsoft Learn)
Summary tables
A) End-to-end steps
| Step | What you implement | Why it matters | Key reference |
|---|---|---|---|
| 1 | Choose v1.0 vs beta | Avoid breaking changes in production | (Microsoft Learn) |
| 2 | Pick delegated vs app-only | Defines token type + permission model | (Microsoft Learn) |
| 3 | Configure + consent permissions | Calls fail without proper consent | (Microsoft Learn) |
| 4 | Use .default in client credentials | Standard app-only token request model | (Microsoft Learn) |
| 5 | Shape queries ($select/$filter/…) | Faster, cheaper, fewer throttles | (Microsoft Learn) |
| 6 | Batch where it helps | Reduce roundtrips (not a magic bullet) | (Microsoft Learn) |
| 7 | Delta for sync | Incremental change tracking | (Microsoft Learn) |
| 8 | Webhooks + lifecycle | Event-driven, fewer polls, stable flow | (Microsoft Learn) |
| 9 | Throttling handling | Required for real tenants at scale | (Microsoft Learn) |
| 10 | Least privilege (Sites.Selected/Selected) | Granular access control | (Microsoft Learn) |
B) Technical takeaways (quick)
| Area | Rule |
|---|---|
| Versioning | v1.0 in prod; beta only for testing (Microsoft Learn) |
| Auth | Delegated = user context; App-only = application permissions (Microsoft Learn) |
.default | Token contains consented app perms for the resource (Microsoft Learn) |
| Performance | Use query parameters to reduce payload (Microsoft Learn) |
| Scale | Batch up to 20; delta for sync (Microsoft Learn) |
| Reliability | Always handle 429 and Retry-After (Microsoft Learn) |
| Least privilege | Selected scopes require explicit assignment after consent (Microsoft Learn) |
