What is Microsoft Graph?
Microsoft Graph is the unified REST API for Microsoft 365. Think of it as a single gateway to access data across SharePoint, Teams, OneDrive, Outlook, Entra ID, and more, all through one consistent API.
Graph vs CSOM/PnP.Framework:
- Graph: Modern REST API, works across all Microsoft 365 services
- CSOM/PnP: SharePoint-specific, more SharePoint features available
When to Use Graph SDK vs PnP.Framework
Reach for the Graph SDK when you need Teams, OneDrive, Exchange or Entra ID data, when you want the built-in retry and batching, or when the feature spans several Microsoft 365 services.
Reach for PnP.Framework when you need the SharePoint-specific surface: site templates, provisioning, taxonomy, search refiners, managed metadata, content types.
Graph won’t help you with SharePoint classic features like master pages and web parts, its search is thinner than the SharePoint Search API, and it can’t talk to SharePoint Server on-premises at all.
Which SDK Version Are You On?
This is the part that costs people an afternoon, so it comes before the code.
The .NET SDK was rewritten in v5. It moved to Kiota-generated clients, and .Request() was removed from every call. That one change means a v4 sample does not compile on v5, and a v5 sample does not compile on v4. Most of the samples you’ll find online, including the first version of this post, are v4.
v6 is the current major, and v5 code compiles on it unchanged. I checked: the same file builds clean against 5.105.0 and 6.5.0. So there are really two dialects to care about, not three.
# current: v5 syntax, v6 package
dotnet add package Microsoft.Graph --version 6.5.0
# legacy: only if you're pinned to v4 already
dotnet add package Microsoft.Graph --version 4.54.0
Pin it. Install-Package Microsoft.Graph with no version gives you the newest major, and if you then paste a v4 sample into it you get a wall of does not contain a definition for 'Request'.
One thing to get out of the way: there is no Microsoft.Graph.Auth package. It never had a stable release and the namespace doesn’t exist in v4, v5 or v6. If a sample tells you to add using Microsoft.Graph.Auth;, that sample predates all of this. You want Azure.Identity.
dotnet add package Azure.Identity
Add Graph Permissions
In your existing app registration, go to “API permissions” then “Add a permission” then “Microsoft Graph” then “Application permissions”:
- Sites.ReadWrite.All: SharePoint sites and lists access
Click “Grant admin consent” after adding permissions, or every call comes back 403.
The Code, v5 and v6
Same certificate and app registration as the PnP.Framework post.
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using System.Security.Cryptography.X509Certificates;
class Program
{
private static readonly string TenantId = "<tenant-id>";
private static readonly string ClientId = "<client-id>";
private static readonly string CertificatePath = @"C:\Temp\cert\appcert.pfx";
private static readonly string CertificatePassword = "<password>";
static async Task Main(string[] args)
{
var certificate = X509CertificateLoader.LoadPkcs12FromFile(CertificatePath, CertificatePassword);
var options = new ClientCertificateCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
};
var credential = new ClientCertificateCredential(TenantId, ClientId, certificate, options);
var scopes = new[] { "https://graph.microsoft.com/.default" };
var graphClient = new GraphServiceClient(credential, scopes);
var org = await graphClient.Organization.GetAsync();
Console.WriteLine($"Connected to: {org?.Value?.FirstOrDefault()?.DisplayName}");
var site = await graphClient.Sites["root"].GetAsync();
var lists = await graphClient.Sites[site.Id].Lists.GetAsync();
Console.WriteLine("Lists in this site:");
foreach (var list in lists?.Value ?? [])
{
Console.WriteLine($"- {list.DisplayName}");
}
var docLib = (lists?.Value ?? []).FirstOrDefault(l => l.DisplayName == "Documents");
if (docLib != null)
{
var content = System.Text.Encoding.UTF8.GetBytes("Hello from Graph SDK!");
var drive = await graphClient.Sites[site.Id].Lists[docLib.Id].Drive.GetAsync();
var file = await graphClient.Drives[drive.Id]
.Items["root:/sample-document.txt:"]
.Content
.PutAsync(new MemoryStream(content));
Console.WriteLine($"Uploaded file: {file?.Name}");
}
}
}
What’s happening here?
ClientCertificateCredentialcomes fromAzure.Identity, not from Graph. It’s aTokenCredential, andGraphServiceClienttakes one directly. No auth provider, no handler, no wrapper..defaultas the scope is what tells Entra ID to issue every application permission you consented to. You don’t list individual scopes for app-only auth.- No
.Request()anywhere. In v5 the request builder ends in the verb, so it’s.GetAsync(),.PutAsync(),.PostAsync()straight off the path. - Collections come back wrapped. It’s
lists.Value, notlists, and everything is nullable, so the compiler will nag you until you handle it. That nagging is correct:Sites["root"]really can hand you back a null. X509CertificateLoaderis .NET 9 and later. On .NET 8 or earlier usenew X509Certificate2(path, password), which still works but is marked obsolete (SYSLIB0057) on newer targets.
The Same Code, v4
If you’re pinned to 4.54.0, the credential setup is identical and only the calls change.
using Azure.Identity;
using Microsoft.Graph;
using System.Security.Cryptography.X509Certificates;
var certificate = new X509Certificate2(CertificatePath, CertificatePassword);
var options = new ClientCertificateCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
};
var credential = new ClientCertificateCredential(TenantId, ClientId, certificate, options);
var scopes = new[] { "https://graph.microsoft.com/.default" };
var graphClient = new GraphServiceClient(credential, scopes);
var org = await graphClient.Organization.Request().GetAsync();
Console.WriteLine($"Connected to: {org.First().DisplayName}");
var site = await graphClient.Sites["root"].Request().GetAsync();
var lists = await graphClient.Sites[site.Id].Lists.Request().GetAsync();
foreach (var list in lists)
{
Console.WriteLine($"- {list.DisplayName}");
}
var docLib = lists.FirstOrDefault(l => l.DisplayName == "Documents");
if (docLib != null)
{
var content = System.Text.Encoding.UTF8.GetBytes("Hello from Graph SDK!");
var drive = await graphClient.Sites[site.Id].Lists[docLib.Id].Drive.Request().GetAsync();
var file = await graphClient.Drives[drive.Id].Root
.ItemWithPath("sample-document.txt")
.Content
.Request()
.PutAsync<DriveItem>(new MemoryStream(content));
Console.WriteLine($"Uploaded file: {file.Name}");
}
Three differences worth naming, because they’re the ones that break a paste:
| v4 | v5 and v6 | |
|---|---|---|
| Call shape | .Request().GetAsync() | .GetAsync() |
| Collections | iterate the result directly | iterate result.Value |
| Upload path | .Root.ItemWithPath("x") | .Items["root:/x:"] |
GraphServiceClient(credential, scopes) is the same in both, which is the good news: the authentication half of this post doesn’t change between versions. Only the calls do.
Key Differences from PnP.Framework
| Operation | PnP.Framework | Graph SDK |
|---|---|---|
| Get Lists | context.Web.Lists | graphClient.Sites[id].Lists |
| Upload File | docLib.RootFolder.Files.Add() | drive.Items["root:/name:"].Content.PutAsync() |
| Authentication | AuthenticationManager | ClientCertificateCredential |
Graph advantages: access to Teams, OneDrive and Exchange data with the same client. PnP advantages: more SharePoint-specific features like content types and site columns.
Troubleshooting
The type or namespace name 'Auth' does not exist in the namespace 'Microsoft.Graph': you copied a sample withusing Microsoft.Graph.Auth;. Delete the line, addAzure.Identity.'OrganizationRequestBuilder' does not contain a definition for 'Request': v4 code on a v5 or v6 package. Drop the.Request()calls.SYSLIB0057: theX509Certificate2constructor is obsolete on .NET 9 and later. UseX509CertificateLoader.LoadPkcs12FromFile.- “Insufficient privileges”: permissions added but admin consent not granted.
- “Certificate not found”: check the path and password before you suspect anything cleverer.
Next Steps
You now have the Graph SDK working with your existing app registration, on whichever major version you’re pinned to. To reach more of Microsoft 365, add the matching application permission:
- User.Read.All for user data
- Group.Read.All for Teams and groups
- Files.ReadWrite.All for broader file operations
- Mail.Read for Exchange data
If you’re still on v4, the upgrade is mostly mechanical: delete every .Request(), add .Value where you iterate, and fix the upload path. The authentication code you just wrote carries over untouched.
