List Items empty when requested via Microsoft Graph

This is something that's taken me a while to figure out from the docs.

Firstly, don't do anything programmatically until you've got it working on the Microsoft Graph Explorer - it is just a big waste of time.

Secondly, the beta version is not ready for your production system , so while it works well, don't rely on it, instead use v1.0 of the REST APIs.

If you know the ID of your site and list, then all URLs will start with one of the following:

https://graph.microsoft.com/v1.0/sites/{siteId}/lists/{listId}/
https://graph.microsoft.com/beta/sites/{siteId}/lists/{listId}/

Note: In the examples below, I give the generic URL, then a real world one which worked for me - so you can see what the format looks like.

If you don't know the listId, lets say we're looking at lists in the root site, we can get them by using this URL in the Microsoft Graph Explorer and click Run Query:

https://graph.microsoft.com/v1.0/sites/{siteId}/lists
https://graph.microsoft.com/v1.0/sites/root/lists

If you want to get all the columns in your list, paste this URL in the Microsoft Graph Explorer and click Run Query

https://graph.microsoft.com/v1.0/sites/{siteId}/lists/{listId}/columns
https://graph.microsoft.com/v1.0/sites/root/lists/ff34268a-d9ff-49c0-99a9-75c6b2eee62e/columns

This returns something similar to:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('root')/lists('ff34268a-d9ff-49c0-99a9-75c6b2eee62e')/columns",
    "value": [
        {
            "columnGroup": "Custom Columns",
            "description": "",
            "displayName": "Title",
            "enforceUniqueValues": false,
            "hidden": false,
            "id": "fa564e0f-0c70-4ab9-b863-0177e6ddd247",
            "indexed": false,
            "name": "Title",
            "readOnly": false,
            "required": true,
            "text": {
                "allowMultipleLines": false,
                "appendChangesToExistingText": false,
                "linesForEditing": 0,
                "maxLength": 255
            }
        },
        ...
    ]
}   

To get the value of what's in your list, use this:

https://graph.microsoft.com/v1.0/sites/{siteId}/lists/{listId}/items?expand=fields
https://graph.microsoft.com/v1.0/sites/root/lists/ff34268a-d9ff-49c0-99a9-75c6b2eee62e/items?expand=fields

Note the expand=fields query which actually adds the values of the items in your list

This returns something similar to:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('root')/lists('ff34268a-d9ff-49c0-99a9-75c6b2eee62e')/items",
    "value": [
        {
            "@odata.etag": "\"6a84a626-dae9-40eb-9c6d-899c6a05ffa8,3\"",
            "createdDateTime": "2017-01-03T11:11:42Z",
            "eTag": "\"6a84a626-dae9-40eb-9c6d-899c6a05ffa8,3\"",
            "id": "1",
            "lastModifiedDateTime": "2017-01-10T18:24:58Z",
            "webUrl": "https://myexample.sharepoint.com/Lists/Some%20Contacts/1_.000",
            "createdBy": {
                "user": {
                    ...
                }
            },
            "lastModifiedBy": {
                "user": {
                    ...
                }
            },
            "parentReference": {},
            "contentType": {
                "id": "0x010062202D579C40994CA18FDBA6760B9545"
            },
            "[email protected]": "https://graph.microsoft.com/v1.0/$metadata#sites('root')/lists('ff34268a-d9ff-49c0-99a9-75c6b2eee62e')/items('1')/fields/$entity",
            "fields": {
                "@odata.etag": "\"6a84a626-dae9-40eb-9c6d-899c6a05ffa8,3\"",
                "Title": "Dr",
                "First_x0020_Name": "David",
                "Surname": "Simpson",
                "Location": "Nottingham",
                "First_x0020_Created": "2017-01-03T08:00:00Z",
                "[email protected]": "#Single",
                "Age": 25,
                "id": "1",
                "ContentType": "Item",
                "Modified": "2017-01-10T18:24:58Z",
                "Created": "2017-01-03T11:11:42Z",
                "AuthorLookupId": "11",
                "EditorLookupId": "11",
                "_UIVersionString": "1.0",
                "Attachments": false,
                "Edit": "",
                "LinkTitleNoMenu": "Dr",
                "LinkTitle": "Dr",
                "ItemChildCount": "0",
                "FolderChildCount": "0",
                "_ComplianceFlags": "",
                "_ComplianceTag": "",
                "_ComplianceTagWrittenTime": "",
                "_ComplianceTagUserId": ""
            }
        },
        ...
    ]
}   

Though I'm using v1.0 of the graph, the beta works just the same.

In my actual app, I'm using offline_access Sites.ReadWrite.All as the scope for the OAuth dance. The former allows for token refresh; the latter for access to SharePoint Online in the Microsoft Graph.

Your authorize URL should look something like this:

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
    ?client_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX
    &response_type=code
    &redirect_uri=https%3A%2F%example.ngrok.io%2Foauth2%2Fcallback
    &response_mode=query
    &scope=offline_access+openid+Sites.ReadWrite.All
    &prompt=consent

An aside: Make sure you are using the Microsoft Graph API (at https://graph.microsoft.com/) rather than the Azure AD Graph API (at https://graph.windows.net/). If you put the wrong scope in your OAuth dance, bad things will happen.

One good thing about using the Microsoft Graph API is that you don't have to bother adding any permissions in the Azure portal beforehand, because you can just add the permissions into the OAuth scope and reauth. This is much easier.