There was a requirement that any user in the organization can update his/her profile picture across all Office 365 apps – the approach was quite straight forward: use Power Apps, save the user data in SharePoint and use Power Automate HTTP connector to do a POST using a Graph API endpoint – however, it seems now that the Power Automate HTTP is a Premium connector which becomes overpriced as all the users in the organization are going to use it, therefore used Azure Web Jobs which did the job well. Note that WebJobs provide an easy way to run scripts or programs as background processes in the context of your app.
Let’s get started.
This is the continuation of Part 1 (Power Apps & SharePoint)
B. Azure Web Jobs & Grap API
B1. Create an Azure Web App to host your code.

– Under Project Details, select Subscription and Resource Group.
– Under Instance Details, provide a descriptive Name, and set as follows – Publish: Code, Runtime stack: latest .Net version, Operating System: Window.

B2. Download the publish profile
Once the Web App is provisionned, Get the publish profile from the Overview menu.

B3. Create a Console Application using Visual Studio 2017 or later.
Once created, right-click on the Project and click Publish as Azure WebJob… to create the Web Job within Visual Studio.
– Provide a descriptive WebJob name.
– Select a WebJob run mode: Run Continuously or Run on Demand. (There is a Scheduled run option as well which we will see in the section B5).

B4. On the next Publish screen, Import the profile settings which you saved in steps B2.

B5. Change the Webjob run mode to Scheduled.
Open the webjob-publish-settings.json within Properties and change the code as follows:
Please note that Scheduled WebJob will be executed based on provided CRON expression. Click here to learn more about CRON Expression.

{
"$schema": "http://schemastore.org/schemas/json/webjob-publish-settings.json",
"webJobName": "WebJobUpdateProfilePicture",
"runMode": "Scheduled",
"schedule": "0 */1 * * * *"
}
B6. Replace Program class with the below code.
– The Program class filters all the SharePoint items which are not yet updated, based on the IsUpdated column.
– Thereafter, ProcessUpdateUserPicture class contains all Graph API (C#) related functions to updated the image to Office 365.
public class Program
{
#region Variables
static StringBuilder logMessage;
static string serviceAccount = "user@myorg.com";
static string serviceAccountPWD = "myPassword";
static string listName = "UsersProfileData";
static string COLUMN_IS_UPDATED = "IsUpdated";
static string COLUMN_EMPLOYEE_AS_TEXT = "EmployeePhotoAsText";
static string COLUMN_TITLE = "Title";
static string COLUMN_EMPLOYEE_UPN = "EmployeeUPN";
#endregion
/// <summary>
/// Main Mthod
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
try
{
Console.WriteLine("Web Job started");
ProcessProfileUpdation("url_of_your_sharepointsite");
Console.WriteLine("Web Job Completed");
}
catch (Exception ex)
{
logMessage.AppendLine(string.Format("Exception occured in Main - {0}", ex.ToString()));
}
finally
{
}
}
/// <summary>
/// Use to process profile updation for all entered users photos
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string ProcessProfileUpdation(string url)
{
try
{
//Authenticate
using (var ctx = new Microsoft.SharePoint.Client.ClientContext(url))
{
var passWord = new SecureString();
foreach (char c in serviceAccountPWD.ToCharArray()) passWord.AppendChar(c);
ctx.Credentials = new SharePointOnlineCredentials(serviceAccount, passWord);
UpdateProfilePhoto(ctx);
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("Exception occured in Main - {0}", ex.ToString()));
}
return string.Empty;
}
/// <summary>
/// Method used to update profile photo
/// </summary>
/// <param name="ctx"></param>
private static void UpdateProfilePhoto(ClientContext ctx)
{
try
{
//Get List
List oList = ctx.Web.Lists.GetByTitle(listName);
//Filter the ones which are not yet Updated
CamlQuery camlQuery = new CamlQuery();
camlQuery.ViewXml = @"<View><Query><Where><Eq><FieldRef Name='IsUpdated' /><Value Type='Boolean'>0</Value></Eq></Where></Query></View>";
ListItemCollection collListItem = oList.GetItems(camlQuery);
ctx.Load(collListItem);
ctx.ExecuteQuery();
foreach (ListItem oListItem in collListItem)
{
bool boolValue = Convert.ToBoolean(oListItem[COLUMN_IS_UPDATED]);
if (boolValue == false)
{
string employeePhotoAsText = oListItem[COLUMN_EMPLOYEE_AS_TEXT].ToString();
string employeeName = oListItem[COLUMN_TITLE].ToString();
string employeeUPN = oListItem[COLUMN_EMPLOYEE_UPN].ToString();
var base64Data = Regex.Match(employeePhotoAsText, @"data:image/(?<type>.+?),(?<data>.+)").Groups["data"].Value;
byte[] bytes = Convert.FromBase64String(base64Data);
System.IO.Stream imageStream = new MemoryStream(bytes);
Stream oldImageStream = new MemoryStream();
bool isUpdated = ProcessUpdateUserPicture.StartUpdation(ctx, employeeUPN, imageStream, out oldImageStream, logMessage);
Console.WriteLine(string.Format("ID: {0} \nEmployeeUPN: {1}", oListItem.Id, employeeUPN));
if (isUpdated == true)
{
oListItem["IsUpdated"] = true;
oListItem.Update();
ctx.ExecuteQuery();
}
oListItem.Update();
ctx.ExecuteQuery();
}
}
}
catch (Exception ex)
{
logMessage.AppendLine("Exception occured at UpdateProfilePhoto: - " + ex.ToString());
Console.WriteLine("Exception occured at UpdateProfilePhoto: - " + ex.ToString());
}
}
}
public class ProcessUpdateUserPicture
{
public static string accessToken = null;
/// <summary>
/// Use to update profile photo for each user
/// </summary>
/// <param name="clientContext">clientContext</param>
/// <param name="userId">employeeUPN</param>
/// <param name="streamImage">IMage stream</param>
/// <param name="logMessage">logMessage</param>
/// <returns></returns>
public static bool StartUpdation(ClientContext clientContext, string userId, Stream streamImage, out Stream previousPhoto, StringBuilder logMessage)
{
bool isUpdated = false;
previousPhoto = new MemoryStream();
try
{
logMessage.AppendLine("Calling GetAuth()");
try
{
GraphServiceClient graphService = GetAuth(clientContext, logMessage);
logMessage.AppendLine("After Calling GetAuth()");
var result = graphService.Users[userId].Photo.Content.Request().PutAsync(streamImage); //users/{1}/photo/$value
do
{
logMessage.AppendLine(string.Format("Result status: {0}", result.Status));
Console.WriteLine("Result status: {0}", result.Status);
Thread.Sleep(20000);
} while (result.Status == System.Threading.Tasks.TaskStatus.WaitingForActivation);
if (result.IsCompleted == true)
{
if (result.Status == System.Threading.Tasks.TaskStatus.RanToCompletion)
{
isUpdated = true;
Console.WriteLine(string.Format("Profile updated for - {0} successfully", userId));
logMessage.AppendLine(string.Format("Profile updated for - {0} successfully", userId));
}
else
{
logMessage.AppendLine(string.Format("Profile process failed for - {0} \nException - {0}", userId, result.Exception.InnerException.Message));
Console.WriteLine(string.Format("Profile process failed for - {0} \nException - {0}", userId, result.Exception.InnerException.Message));
}
}
}
catch (Microsoft.Graph.ServiceException svcEx)
{
var additionalData = svcEx.Error.AdditionalData;
logMessage.AppendLine(string.Format("Microsoft.Graph.ServiceException svcEx - {0}", additionalData["details"].ToString()));
}
}
catch (Exception ex)
{
logMessage.AppendLine(ex.ToString());
Console.WriteLine(ex.ToString());
}
return isUpdated;
}
/// <summary>
/// Access token for Graph API call
/// </summary>
/// <param name="logMessage"></param>
/// <returns></returns>
private static string GetAccessToken(StringBuilder logMessage)
{
try
{
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Globals.AuthorityUrl, true);
Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential creds = new Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential(Globals.ClentId, Globals.ClientSecret);
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult authenticationResult = authContext.AcquireTokenAsync(Globals.GraphResourceUrl, creds).Result;
return authenticationResult.AccessToken;
}
catch (Exception ex)
{
logMessage.AppendLine(string.Format("Exception occured at GetAccessToken - {0}", ex.ToString()));
}
return null;
}
/// <summary>
/// Method used for Graph API call
/// </summary>
/// <param name="clientContext"></param>
/// <param name="logMessage"></param>
/// <returns></returns>
private static GraphServiceClient GetAuth(ClientContext clientContext, StringBuilder logMessage)
{
try
{
accessToken = GetAccessToken(logMessage);
GraphServiceClient graphClient = GetGraphClient(clientContext, accessToken, logMessage);
return graphClient;
}
catch (Exception ex)
{
logMessage.AppendLine(string.Format("Exception occured at GetAuth - {0}", ex.ToString()));
//CustomLogs.LogError(clientContext, string.Format("Exception from GetAuth"), ex);
}
return null;
}
/// <summary>
/// Method used for Graph API call
/// </summary>
/// <param name="clientContext"></param>
/// <param name="graphToken"></param>
/// <param name="logMessage"></param>
/// <returns></returns>
public static GraphServiceClient GetGraphClient(ClientContext clientContext, string graphToken, StringBuilder logMessage)
{
try
{
DelegateAuthenticationProvider authenticationProvider = new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", graphToken);
return Task.FromResult(0);
});
return new GraphServiceClient(authenticationProvider);
}
catch (Exception ex)
{
logMessage.AppendLine(string.Format("Exception from GetGraphClient - {0} ", ex.ToString()));
}
return null;
}
}
B7. Deploy the code to the Azure Web App.
In Visual Studio, right-click on the Project, choose to Publish as Azure WebJob then click on Publish to deploy the code.
Once successfully deployed, in the Azure Web App, the job entry must be present within WebJobs – the job is run to scheduled every 15mn in this case.

To ensure the job has run successfuly, click on Logs to see more details.
Cheers!