Microsoft Graph vs. SharePoint Online APIs (and where PnP fits): an exhaustive developer’s guide

SharePoint Online is exposed through two primary “front doors”:

  1. Microsoft Graph (the “unified” Microsoft 365 API gateway)
  2. 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.Selected starts with no access until explicitly granted permissions on a target list/site (Microsoft Learn).
  • For SharePoint sites via Graph, Sites.Selected is 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

ScenarioEndpointNotesExample (HTTP)
Search sitesGET /sites?search={query}Tenant-wide keyword search (Microsoft Learn)GET https://graph.microsoft.com/v1.0/sites?search=ExampleSite
Get site by pathGET /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 siteGET /sites/{site-id}/listsLists + libraries as lists (Microsoft Learn)GET https://graph.microsoft.com/v1.0/sites/{site-id}/lists
Get list metadataGET /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=fieldsSupports $filter on fields (Microsoft Learn)GET .../items?$expand=fields($select=Title,Status)&$filter=fields/Status eq 'Active'
Create list itemPOST /sites/{site-id}/lists/{list-id}/itemsBody uses fields (Microsoft Learn)(see C# below)
List drives (doc libs)GET /sites/{site-id}/drivesDrives on site (Microsoft Learn)GET https://graph.microsoft.com/v1.0/sites/{site-id}/drives
Get driveItem metadataGET /drives/{drive-id}/items/{item-id}Also supports path forms (Microsoft Learn)GET .../drives/{drive-id}/items/{item-id}
List folder childrenGET /drive/items/{item-id}/childrenEnumerate folder (Microsoft Learn)GET .../drives/{drive-id}/items/{item-id}/children

B) SharePoint REST — list CRUD basics

ScenarioEndpointNotesExample (HTTP)
Get list itemsGET https://{site}/_api/web/lists/getbytitle('{list}')/itemsClassic list read (Microsoft Learn)GET https://contoso.sharepoint.com/sites/ExampleSite/_api/web/lists/getbytitle('Example List')/items
Update item with ETagMERGE/PUT .../items({id})Use If-Match / ETag patterns (Microsoft Learn)If-Match: * for overwrite
Delete item with ETagDELETE .../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.Graph
  • Azure.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:

  1. 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).
  2. 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:

  1. Microsoft Graph documentation hub (Microsoft Learn)
  2. SharePoint API in Microsoft Graph (capabilities & limits) (Microsoft Learn)
  3. Microsoft Graph SDK overview (patterns, libraries) (Microsoft Learn)
  4. List items in Graph (filtering, expand/fields) (Microsoft Learn)
  5. Create list item in Graph (Microsoft Learn)
  6. Search for sites in Graph (Microsoft Learn)
  7. Drives: list drives / list children / driveItem (Microsoft Learn)
  8. Graph permissions reference (delegated/app perms) (Microsoft Learn)
  9. Selected permissions overview (least privilege model) (Microsoft Learn)
  10. SharePoint REST list CRUD + ETags (Microsoft Learn)
  11. PnP provisioning framework concepts (Microsoft Learn)
  12. SPFx: connect to SharePoint using PnPjs (Microsoft Learn)

Summary tables

A) Step summary (implementation flow)

StepWhat you doWhy it matters
1Pick API surface (Graph vs SharePoint REST vs PnP)Prevents dead-ends (e.g., site creation vs list CRUD)
2Choose auth model (delegated/app-only, Selected perms)Security + compliance + real-world deployability (Microsoft Learn)
3Resolve site + list IDsMost Graph operations require IDs (Microsoft Learn)
4Query with OData ($filter/$select/$expand)Push work server-side (Microsoft Learn)
5Use LINQ locally for complex shapingFaster iteration, fewer API roundtrips
6Write items / filesUse Graph list item create, drives endpoints (Microsoft Learn)

B) Technical decision matrix (quick pick)

NeedBest starting pointWhy
Cross-M365 data + SharePoint lists/filesGraph SDKUnified API + strong typing (Microsoft Learn)
SharePoint-only features / legacy endpointsSharePoint RESTDeep SharePoint coverage (Microsoft Learn)
.NET automation with minimal API jugglingPnP Core SDKAbstracts Graph vs REST, Graph-first reads (Microsoft 365 & Power Platform Community)
Provisioning templates, repeatable site artifactsPnP FrameworkProvisioning engine + CSOM/Graph/REST combo (Microsoft 365 & Power Platform Community)
SPFx client-side fluent callsPnPjsRecommended SPFx-friendly approach (Microsoft Learn)
Edvaldo Guimrães Filho Avatar

Published by