I am trying to expose a REST API using Azure Functions which returns terms from a specific termset in SharePoint Online using CSOM and C#.
I can definitely invoke this exact same CSOM code from a console app and it is able to loop through the terms and output to console successfully.
However, when the code below is invoked from the Azure Function host, it ALWAYS find a collection of NULL term objects, when looping through the TermCollection
or the IEnumerable<Term>
(I’ve tried by using ClientContext.LoadQuery
on TermSet.GetAllTerms()
, as well as by just loading the TermCollection
via the TermSet.Terms
property).
As soon as the iterator hits a term in the foreach (which I’ve also tried as just a LINQ Select), it thinks that the item is NULL, so calling properties on it throws the NullReferenceException
. I cannot reproduce the behavior from the console app calling into the same code - it just works as expected there and retrieves each Term
object.
Why would this happen in the Azure Functions host, but not in Console app? How can I fix this??
UPDATE: Someone marked this question as a Duplicate of "what is an NRE". I know what and how NREs occur. The question here is why is this happening when SAME CODE is invoked from different hosts?? I know the exact line where the NRE is thrown, and what object is throwing it - but it should not be, as this code works as is from a console app. What is the difference when invoked from an Azure Function host??
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.Taxonomy;
namespace CsomTaxonomyHelper
{
public class TermSearch
{
private readonly ClientContext ctx;
public TermSearch(ClientContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
ctx = context;
}
public IEnumerable<TermViewModel> GetTerms(Guid termSetId)
{
var taxonomySession = TaxonomySession.GetTaxonomySession(ctx);
var termStore = taxonomySession.GetDefaultSiteCollectionTermStore();
var termSet = termStore.GetTermSet(termSetId);
//get flat list of terms, so we don't make recursive calls to SPO
var allTerms = ctx.LoadQuery(termSet.GetAllTerms().IncludeWithDefaultProperties());
ctx.ExecuteQuery();
return ToViewModel(allTerms);
}
static IEnumerable<TermViewModel> ToViewModel(IEnumerable<Term> allTerms)
{
var results = allTerms.Select(term => new TermViewModel
{
Id = term.Id, //BOOM! <-- within the context of an Azure Function the "allTerms" IEnumerable is a list of nulls
Name = term.Name,
ParentId = TryGetParentId(term)
});
return results;
}
static Guid? TryGetParentId(Term term)
{
try
{
if (term.Parent.IsPropertyAvailable("Id"))
return term.Parent.Id;
}
catch (ServerObjectNullReferenceException) { }
return null;
}
}
public class PasswordString
{
public SecureString SecurePassword { get; private set; }
public PasswordString(string password)
{
SecurePassword = new SecureString();
foreach (char c in password.ToCharArray())
{
SecurePassword.AppendChar(c);
}
SecurePassword.MakeReadOnly();
}
}
}
Here's the "run.csx" function, invoking the code above which has been compiled into a DLL and placed in the Bin folder of the Azure Function:
#r "CsomTaxonomyHelper.dll"
#r "Newtonsoft.Json"
using System.Net;
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.Taxonomy;
using CsomTaxonomyHelper;
using Newtonsoft.Json;
static TraceWriter _log = null;
public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log)
{
_log = log;
_log.Info("C# HTTP trigger function processed a request. Getting mmd terms from SPO...");
var terms = GetFocusAreas();
var result = JsonConvert.SerializeObject(terms);
return req.CreateResponse(HttpStatusCode.OK, result);
}
static IEnumerable<TermViewModel> GetFocusAreas()
{
string spSiteUrl = System.Environment.GetEnvironmentVariable("SPOSiteUrl", EnvironmentVariableTarget.Process);
string userName = System.Environment.GetEnvironmentVariable("SPOUserName", EnvironmentVariableTarget.Process);
string password = System.Environment.GetEnvironmentVariable("SPOPassword", EnvironmentVariableTarget.Process);
var securePwd = new PasswordString(password).SecurePassword;
using (var ctx = new ClientContext(spSiteUrl))
{
ctx.Credentials = new SharePointOnlineCredentials(userName, securePwd);
ctx.ExecuteQuery();
_log.Info("Logged into SPO service.");
var search = new TermSearch(ctx);
try
{
var result = search.GetTerms(new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"));
return result;
}
catch (Exception ex)
{
_log.Error(ex.Message, ex);
throw;
}
}
}
Here's the screenshot of the local debugger, when using the Azure Functions CLI to debug this (you can see that it did find 10 items in the collection, but all items are null):