Microsoft Graph vs. SharePoint Online APIs (and where PnP fits): an exhaustive developer’s guide
SharePoint Online is exposed through two primary “front doors”:
- Microsoft Graph (the “unified” Microsoft 365 API gateway)
- SharePoint-native APIs (SharePoint REST/OData and legacy client object models)
Your development choice is rarely “either/or.” In real projects, you’ll often combine them: Graph for cross-M365 consistency (users, groups, files, lists) and SharePoint-native for the SharePoint-only corners Graph doesn’t cover (or doesn’t cover well yet). Microsoft 365 PnP libraries sit on top and help you avoid reinventing plumbing.
1) What Graph is for SharePoint (and what it is not)
What Graph SharePoint API is great at
Graph’s SharePoint API focuses on:
- Sites, lists, list items, and drives (document libraries) as core resources (Microsoft Learn)
- Read/write for lists, list items, drive items, while site resources are read-only (notably, you can’t create new sites through Graph SharePoint API) (Microsoft Learn)
- Consistent auth, SDKs, batching patterns, and cross-product integration (Teams/Outlook/Entra data in the same API surface) (Microsoft Learn)
Key limitation to internalize early: site creation
Microsoft’s own Graph SharePoint docs and community answers repeatedly confirm the practical limitation: Graph doesn’t provide “create SharePoint site” capability in the SharePoint API surface (Microsoft Learn).
If your solution must provision sites, you’ll typically use:
- SharePoint REST site creation endpoints or admin APIs
- PnP provisioning (PnP Framework / PnP PowerShell)
- CSOM (where still applicable)
2) The SharePoint-native options (REST/OData, CSOM) and why they still matter
SharePoint REST (OData)
SharePoint’s REST API is still a workhorse for:
- list CRUD
- files/folders operations
- SharePoint-specific endpoints and behaviors (ETags, metadata, etc.)
Microsoft’s “Working with lists and list items with REST” doc is still one of the best references, especially for update semantics with ETags and concurrency control (Microsoft Learn).
CSOM (Client-Side Object Model)
CSOM is older, but still used via tooling and frameworks for:
- deep SharePoint provisioning patterns
- complex SharePoint constructs not uniformly available via Graph
You’ll see it heavily in provisioning stacks, modernization utilities, and some administrative automation.
3) Development options compared (what to choose and when)
Option A — Microsoft Graph REST (raw HttpClient)
Use when
- You want full control, minimal dependencies, easiest debugging of raw requests.
Tradeoffs
- You write pagination, retries, throttling handling, and request building manually.
Option B — Microsoft Graph .NET SDK
Microsoft Graph SDKs provide a typed, discoverable experience and common pipeline behaviors (Microsoft Learn).
Use when
- You’re building a serious C# service/app and want strong typing + request builders.
Tradeoffs
- You must learn SDK patterns, and some advanced query shapes still require careful expand/select usage.
Option C — SharePoint REST (raw)
Use when
- You need SharePoint-only endpoints or behaviors not supported/comfortable in Graph.
- You’re already in SharePoint context (SPFx, classic integrations).
Tradeoffs
- Not unified with other M365 workloads (users/groups/etc.), different patterns than Graph.
Option D — PnP Core SDK (recommended for modern .NET SharePoint automation)
PnP Core SDK provides a unified object model and can transparently call Graph or SharePoint REST underneath, favoring Graph-first when possible (Microsoft 365 & Power Platform Community).
Use when
- You want “I want a Web/List/Item, I don’t care which endpoint fetches it.”
Tradeoffs
- It’s an abstraction layer; you still need to understand underlying API limits to debug edge cases.
Option E — PnP Framework (provisioning engine powerhouse)
PnP Framework is a cross-platform successor to older PnP libraries and includes the PnP Provisioning Engine (Microsoft 365 & Power Platform Community).
It also mixes CSOM + Graph + SharePoint REST where needed (Microsoft for Developers).
Use when
- Your main problem is provisioning site artifacts (fields, content types, lists, pages, etc.) via templates and repeatable deployment (Microsoft Learn).
Option F — SPFx + PnPjs (front-end)
For SPFx, Microsoft highlights using PnPjs to connect safely to SharePoint/M365 APIs with a fluent JS API (Microsoft Learn).
4) How PnP relates to Graph (mental model)
Think of PnP as “developer productivity layers”:
- PnP Core SDK (C#): abstraction over Graph + SharePoint REST (and other endpoints). It defaults to Graph-first for reads when Graph supports the requested properties (Microsoft 365 & Power Platform Community).
- PnP Framework (C#): provisioning-first library (templates, applying artifacts), combining CSOM plus Graph/REST where CSOM falls short (Microsoft for Developers).
- PnPjs (JS/SPFx): fluent, safe consumption of SharePoint/M365 APIs in client-side solutions (Microsoft Learn).
Practical takeaway:
- If you’re building automation services in .NET, start with PnP Core SDK unless you have strong reasons not to.
- If you’re building repeatable provisioning pipelines, evaluate PnP Framework provisioning engine.
5) Authentication + permissions (the part that makes or breaks everything)
Graph permissions are expressed as delegated (user) or application (app-only) permissions (Microsoft Learn).
The modern “least privilege” path: Selected scopes
Microsoft introduced “Selected” permissions for granular access in OneDrive/SharePoint and lists:
- Example: an app with
Lists.SelectedOperations.Selectedstarts with no access until explicitly granted permissions on a target list/site (Microsoft Learn). - For SharePoint sites via Graph,
Sites.Selectedis the well-known approach for resource-specific consent (RSC) patterns (Microsoft Learn).
When to care
- Vendor apps, security-driven tenants, or internal tools that must not “see everything.”
6) Endpoint & example table (Graph + SharePoint REST)
Placeholders used below:
- Tenant:
contoso.sharepoint.com- Site:
/sites/ExampleSite- List:
Example List
A) Microsoft Graph (v1.0) — common SharePoint operations
| Scenario | Endpoint | Notes | Example (HTTP) |
|---|---|---|---|
| Search sites | GET /sites?search={query} | Tenant-wide keyword search (Microsoft Learn) | GET https://graph.microsoft.com/v1.0/sites?search=ExampleSite |
| Get site by path | GET /sites/{host}:/sites/{path} | Convenient site addressing (Microsoft Learn) | GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/ExampleSite |
| List lists in a site | GET /sites/{site-id}/lists | Lists + libraries as lists (Microsoft Learn) | GET https://graph.microsoft.com/v1.0/sites/{site-id}/lists |
| Get list metadata | GET /sites/{site-id}/lists/{list-id} | Can expand columns/items (Microsoft Learn) | GET .../lists/{list-id}?$expand=columns |
| List list items (+fields) | GET /sites/{site-id}/lists/{list-id}/items?$expand=fields | Supports $filter on fields (Microsoft Learn) | GET .../items?$expand=fields($select=Title,Status)&$filter=fields/Status eq 'Active' |
| Create list item | POST /sites/{site-id}/lists/{list-id}/items | Body uses fields (Microsoft Learn) | (see C# below) |
| List drives (doc libs) | GET /sites/{site-id}/drives | Drives on site (Microsoft Learn) | GET https://graph.microsoft.com/v1.0/sites/{site-id}/drives |
| Get driveItem metadata | GET /drives/{drive-id}/items/{item-id} | Also supports path forms (Microsoft Learn) | GET .../drives/{drive-id}/items/{item-id} |
| List folder children | GET /drive/items/{item-id}/children | Enumerate folder (Microsoft Learn) | GET .../drives/{drive-id}/items/{item-id}/children |
B) SharePoint REST — list CRUD basics
| Scenario | Endpoint | Notes | Example (HTTP) |
|---|---|---|---|
| Get list items | GET https://{site}/_api/web/lists/getbytitle('{list}')/items | Classic list read (Microsoft Learn) | GET https://contoso.sharepoint.com/sites/ExampleSite/_api/web/lists/getbytitle('Example List')/items |
| Update item with ETag | MERGE/PUT .../items({id}) | Use If-Match / ETag patterns (Microsoft Learn) | If-Match: * for overwrite |
| Delete item with ETag | DELETE .../items({id}) | Concurrency-safe deletion (Microsoft Learn) | If-Match: {etag} |
7) C# with Microsoft Graph SDK: SharePoint lists + files (step-by-step + full code)
Below is a single-file console app example that demonstrates:
- Resolve a SharePoint site by path
- Resolve a list by display name
- Query list items (server-side filter via OData)
- Apply LINQ locally for advanced shaping
- Create a new list item
This code uses placeholders and does not include any tenant-specific values.
Step 1 — NuGet packages
Install:
Microsoft.GraphAzure.Identity
Step 2 — App registration basics
- Create an app registration in Microsoft Entra ID
- Decide delegated vs application permissions
- Grant the least privileges needed (consider “Selected” permissions where appropriate) (Microsoft Learn)
Step 3 — Full C# code (Program.cs)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Models;
internal static class Program
{
// ------------------------------------------------------------
// CONFIG (placeholders)
// ------------------------------------------------------------
private const string TenantId = "YOUR_TENANT_ID";
private const string ClientId = "YOUR_CLIENT_ID";
private const string ClientSecret = "YOUR_CLIENT_SECRET";
// SharePoint target (placeholders)
private const string SiteHostName = "contoso.sharepoint.com";
private const string SitePath = "/sites/ExampleSite";
private const string ListDisplayName = "Example List";
// Graph scope for app-only:
private static readonly string[] GraphScopes = new[] { "https://graph.microsoft.com/.default" };
public static async Task Main()
{
try
{
var graph = CreateGraphClient();
// 1) Resolve site by path
var site = await graph.Sites[$"{SiteHostName}:{SitePath}"].GetAsync();
if (site == null || string.IsNullOrWhiteSpace(site.Id))
throw new InvalidOperationException("Site not found (check host/path).");
Console.WriteLine($"Site resolved: {site.WebUrl}");
Console.WriteLine($"SiteId: {site.Id}");
// 2) Find the list by display name
var lists = await graph.Sites[site.Id].Lists.GetAsync();
if (lists?.Value == null)
throw new InvalidOperationException("No lists returned.");
var targetList = lists.Value.FirstOrDefault(l =>
string.Equals(l.DisplayName, ListDisplayName, StringComparison.OrdinalIgnoreCase));
if (targetList == null || string.IsNullOrWhiteSpace(targetList.Id))
throw new InvalidOperationException($"List not found: {ListDisplayName}");
Console.WriteLine($"List resolved: {targetList.DisplayName} ({targetList.Id})");
// 3) Query list items with fields
// Server-side filtering: Graph supports filtering on listItem fields :contentReference[oaicite:30]{index=30}
var itemsPage = await graph.Sites[site.Id]
.Lists[targetList.Id]
.Items
.GetAsync(cfg =>
{
cfg.QueryParameters.Top = 50;
cfg.QueryParameters.Expand = new[]
{
"fields($select=Title,Status,Created)"
};
cfg.QueryParameters.Filter = "fields/Status eq 'Active'";
});
var items = itemsPage?.Value ?? new List<ListItem>();
// 4) LINQ locally (advanced shaping, computed fields, etc.)
// Graph returns column values in fieldValueSet :contentReference[oaicite:31]{index=31}
var projected = items
.Select(i => new
{
Id = i.Id,
Title = GetFieldAsString(i, "Title"),
Status = GetFieldAsString(i, "Status"),
Created = GetFieldAsDateTime(i, "Created")
})
.Where(x => !string.IsNullOrWhiteSpace(x.Title))
.OrderByDescending(x => x.Created)
.ToList();
Console.WriteLine();
Console.WriteLine("Active items (top 50, ordered by Created):");
foreach (var row in projected)
{
Console.WriteLine($"- {row.Id} | {row.Title} | {row.Status} | {row.Created:O}");
}
// 5) Create a new list item :contentReference[oaicite:32]{index=32}
var newItem = new ListItem
{
Fields = new FieldValueSet
{
AdditionalData = new Dictionary<string, object>
{
["Title"] = "Created by Graph SDK (console)",
["Status"] = "Active"
}
}
};
var created = await graph.Sites[site.Id]
.Lists[targetList.Id]
.Items
.PostAsync(newItem);
Console.WriteLine();
Console.WriteLine($"Created item Id: {created?.Id}");
}
catch (Exception ex)
{
Console.WriteLine("ERROR:");
Console.WriteLine(ex);
}
}
private static GraphServiceClient CreateGraphClient()
{
// App-only credential flow.
// Ensure your app registration has the right Graph permissions granted by admin. :contentReference[oaicite:33]{index=33}
var credential = new ClientSecretCredential(TenantId, ClientId, ClientSecret);
return new GraphServiceClient(credential, GraphScopes);
}
private static string GetFieldAsString(ListItem item, string fieldName)
{
if (item?.Fields?.AdditionalData == null) return "";
if (!item.Fields.AdditionalData.TryGetValue(fieldName, out var value)) return "";
return value?.ToString() ?? "";
}
private static DateTime GetFieldAsDateTime(ListItem item, string fieldName)
{
var raw = GetFieldAsString(item, fieldName);
if (DateTime.TryParse(raw, out var dt)) return dt;
return DateTime.MinValue;
}
}
“LINQ for SharePoint Online” — what this really means
There are two layers:
- Server-side filtering/projection via Graph OData (
$filter,$select,$expand)
This is your “database-like” query pushdown. Graph explicitly documents filtering list items and fields (Microsoft Learn). - Client-side LINQ after retrieval
Use LINQ for transformations Graph can’t do (complex joins, fuzzy logic, scoring, normalization, etc.).
8) PnP Core SDK vs Graph SDK: when PnP gives you leverage
PnP Core SDK can decide whether to call Graph or SharePoint REST based on what property you request, and it defaults Graph-first for reads when possible (Microsoft 365 & Power Platform Community). That’s powerful when:
- you want a clean domain model
- you don’t want to maintain 2+ separate client stacks
- you accept an abstraction layer for speed of development
If you are building a migration/provisioning pipeline, PnP Framework is often the better fit because it’s built around provisioning artifacts and templates (Microsoft 365 & Power Platform Community) and is explicitly designed to mix CSOM + Graph + REST when required (Microsoft for Developers).
9) Recommended Microsoft Learn reading path (curated)
These are the most useful “anchor” docs to keep open while building:
- Microsoft Graph documentation hub (Microsoft Learn)
- SharePoint API in Microsoft Graph (capabilities & limits) (Microsoft Learn)
- Microsoft Graph SDK overview (patterns, libraries) (Microsoft Learn)
- List items in Graph (filtering, expand/fields) (Microsoft Learn)
- Create list item in Graph (Microsoft Learn)
- Search for sites in Graph (Microsoft Learn)
- Drives: list drives / list children / driveItem (Microsoft Learn)
- Graph permissions reference (delegated/app perms) (Microsoft Learn)
- Selected permissions overview (least privilege model) (Microsoft Learn)
- SharePoint REST list CRUD + ETags (Microsoft Learn)
- PnP provisioning framework concepts (Microsoft Learn)
- SPFx: connect to SharePoint using PnPjs (Microsoft Learn)
Summary tables
A) Step summary (implementation flow)
| Step | What you do | Why it matters |
|---|---|---|
| 1 | Pick API surface (Graph vs SharePoint REST vs PnP) | Prevents dead-ends (e.g., site creation vs list CRUD) |
| 2 | Choose auth model (delegated/app-only, Selected perms) | Security + compliance + real-world deployability (Microsoft Learn) |
| 3 | Resolve site + list IDs | Most Graph operations require IDs (Microsoft Learn) |
| 4 | Query with OData ($filter/$select/$expand) | Push work server-side (Microsoft Learn) |
| 5 | Use LINQ locally for complex shaping | Faster iteration, fewer API roundtrips |
| 6 | Write items / files | Use Graph list item create, drives endpoints (Microsoft Learn) |
B) Technical decision matrix (quick pick)
| Need | Best starting point | Why |
|---|---|---|
| Cross-M365 data + SharePoint lists/files | Graph SDK | Unified API + strong typing (Microsoft Learn) |
| SharePoint-only features / legacy endpoints | SharePoint REST | Deep SharePoint coverage (Microsoft Learn) |
| .NET automation with minimal API juggling | PnP Core SDK | Abstracts Graph vs REST, Graph-first reads (Microsoft 365 & Power Platform Community) |
| Provisioning templates, repeatable site artifacts | PnP Framework | Provisioning engine + CSOM/Graph/REST combo (Microsoft 365 & Power Platform Community) |
| SPFx client-side fluent calls | PnPjs | Recommended SPFx-friendly approach (Microsoft Learn) |
