Batch Update User Profile Properties

%3CLINGO-SUB%20id%3D%22lingo-sub-16221%22%20slang%3D%22en-US%22%3EBatch%20Update%20User%20Profile%20Properties%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-16221%22%20slang%3D%22en-US%22%3E%3CP%3EA%20few%20months%20back%2C%20the%20Patterns%20and%20Practices%20team%20at%20Microsoft%20announced%20a%20new%20%3CA%20href%3D%22http%3A%2F%2Fdev.office.com%2Fpatterns-and-practices-detail%2F7202%22%20target%3D%22_self%22%20rel%3D%22noopener%20noreferrer%22%3EUser%20Profile%20Batch%20Update%20API%3C%2FA%3E.%20%26nbsp%3BIt's%20great%20for%20updating%20large%20batches%20of%20properties%2C%20but%26nbsp%3B%3CSPAN%3Eonly%20works%20for%20user%20profile%20properties%2C%20which%20have%20not%20been%20set%20to%20be%20editable%20for%20the%20end%20users%20to%20avoid%20situation%20where%20the%20user%20profile%20import%20process%20would%20override%20any%20information%20which%20end%20user%20has%20already%20updated.%20%26nbsp%3BThis%20makes%20a%20lot%20of%20sense.%20%26nbsp%3BBut%20what%20if%20you%20need%20to%20do%20some%20changes%20to%20these%20editable%20fields%20for%20a%20number%20of%20users%3F%3C%2FSPAN%3E%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3E%3CSPAN%3ETo%20do%20just%20that%2C%20I%20wrote%20a%20script%20using%20some%20of%20the%20%3CA%20href%3D%22https%3A%2F%2Fgithub.com%2FOfficeDev%2FPnP-PowerShell%22%20target%3D%22_self%22%20rel%3D%22noopener%20noreferrer%22%3EOfficeDev%20PnP%20PowerShell%3C%2FA%3E%20library.%20%26nbsp%3BIn%20order%20to%20use%20it%2C%20you%20need%20to%20consider%20the%20following%3A%3C%2FSPAN%3E%3C%2FP%3E%3CUL%3E%3CLI%3E%3CSPAN%3EThe%20input%20is%20a%20CSV%20file%20(sample%20attached%20to%20the%20post)%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3ELog%20onto%20the%20admin%20site%20(e.g.%20https%3A%2F%2F%3CEM%3Etenant%3C%2FEM%3E-admin.sharepoint.com)%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3EThe%20first%20row%20of%20the%20CSV%20must%20be%20the%20InternalName%20properties%20of%20the%20fields%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3EThe%20first%20column%20should%20be%20the%20Email%20of%20the%20user%20you're%20modifying%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3EIf%20you%20want%20to%20add%20multiple%20values%20to%20a%20field%2C%20they%20should%20be%20separated%20by%20a%20pipe%20(%7C)%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3EFor%20People%20Picker%20fields%2C%20use%20the%20AccountName%20(e.g.%20i%3A0%23.f%7Cmembership%7Croby%40%3CEM%3Etenant%3C%2FEM%3E.onmicrosoft.com)%3C%2FSPAN%3E%3C%2FLI%3E%3CLI%3E%3CSPAN%3ESince%20querying%20user%20profile%20property%20fields%20returns%20all%20the%20values%20as%20text%2C%20the%20code%20doesn't%20check%20for%20field%20types%20and%20tries%20to%20save%20the%20values%20provided%20as-is.%20%26nbsp%3BIf%20it%20fails%2C%20it%20will%20report%20an%20error%20to%20the%20console.%3C%2FSPAN%3E%3C%2FLI%3E%3C%2FUL%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CPRE%3EFunction%20PopulateUserProfiles%0A%7B%0A%5BCmdletBinding()%5D%0A%20param%0A%20(%0A%20%5BParameter(Position%3D1%2CMandatory%3D%24true)%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Bstring%5D%20%24csvFile%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20try%0A%20%20%20%20%7B%0A%20%20%24ctx%20%3D%20Get-SPOContext%0A%20%20%20%20%7D%0A%20%20%20%20catch%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20Write-Host%20%22Please%20connect%20to%20tenant%20admin%20site%20before%20running%20this%20function.%22%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%7D%0A%0A%20%20%20%20%20if%20(Test-Path%20%24csvFile)%0A%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%24UserData%20%3D%20Import-Csv%20%24csvFile%0A%20%20%20%20%20%24rows%20%3D%20%24UserData%20%7C%20measure%0A%20%20%20%20%20%24ColumnName%20%3D%20%24UserData%20%7C%20get-member%20%7C%20%3F%20%7B-not(%24_.Name%20-in%20%40(%22Equals%22%2C%20%22GetHashCode%22%2C%20%22GetType%22%2C%20%22ToString%22))%7D%20%7C%20select%20%22Name%22%0A%0A%20%20%20%20%20for%20(%24i%20%3D%200%3B%20%24i%20-lt%20%24rows.Count%3B%20%24i%2B%2B)%0A%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24Email%20%3D%20%24UserData%5B%24i%5D.(%24ColumnName%5B0%5D.Name)%0A%20%20%20%20%20%20%20%20%20%20%20%20Write-Host%20%22Updating%20data%20for%20%24Email%22%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(%24j%20%3D%201%3B%20%24j%20-lt%20%24ColumnName.Count%3B%20%24j%2B%2B)%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20if%20there%20are%20multiple%20values%2C%20then%20I%20separate%20them%20using%20the%20%7C.%20%20However%2C%20if%20a%20person%20field%20is%20used%2C%20then%20the%20format%20is%20using%20the%20claims%20encoding%20which%20also%20includes%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20the%20%7C.%20%20So%20first%20I%20check%20if%20it%20is%20a%20user%20account%20or%20not%20and%20act%20accordingly.%20%20See%20http%3A%2F%2Fwww.wictorwilen.se%2FPost%2FHow-Claims-encoding-works-in-SharePoint-2010.aspx%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20for%20more%20information%20about%20the%20claims%20format.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24value%20%3D%20%20%24UserData%5B%24i%5D.(%24ColumnName%5B%24j%5D.Name)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20if%20the%20first%20characters%20are%20i%3A0%20or%20c%3A0%2C%20then%20it's%20a%20user%20account%20and%20I%20leave%20it%20as-is.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20((%24value.Substring(0%2C3)%20-eq%20%22i%3A0%22)%20-or%20(%24value.SubString(0%2C3)%20-eq%20%22c%3A0%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Set-SPOUserProfileProperty%20-account%20%24Email%20-PropertyName%20%24ColumnName%5B%24j%5D.Name%20-Values%20%24value%20-ErrorAction%20SilentlyContinue%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20split%20the%20string%20using%20the%20%7C%20as%20a%20delimiter%20and%20load%20the%20values%20into%20the%20field.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Set-SPOUserProfileProperty%20-account%20%24Email%20-PropertyName%20%24ColumnName%5B%24j%5D.Name%20-Values%20%24value.Split(%22%7C%22)%20-ErrorAction%20SilentlyContinue%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(%24%3F)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Write-Host%20%22%20%20Set%20%24(%24ColumnName%5B%24j%5D.Name)%20--%26gt%3B%20%24(%24UserData%5B%24i%5D.(%24ColumnName%5B%24j%5D.Name)).%22%20-ForegroundColor%20Green%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Write-Host%20%22%20%20Could%20not%20set%20%24(%24ColumnName%5B%24j%5D.Name)%20--%26gt%3B%20%24(%24UserData%5B%24i%5D.(%24ColumnName%5B%24j%5D.Name)).%20%20%24(%24error%5B0%5D.Exception.message)%22%20-ForegroundColor%20Red%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%7D%0A%20%20%20%20%20%7D%0A%7D%3C%2FPRE%3E%3CP%3EI%20have%20tested%20out%20the%20code%20on%20a%20number%20scenarios%20that%20worked.%20%26nbsp%3BIf%20you%20find%20any%20issues%2C%20please%20let%20me%20know%20and%20I'll%20try%20to%20correct%20them.%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EI%20hope%20you%20find%20it%20helpful!%3C%2FP%3E%3C%2FLINGO-BODY%3E%3CLINGO-LABS%20id%3D%22lingo-labs-16221%22%20slang%3D%22en-US%22%3E%3CLINGO-LABEL%3EAdmin%3C%2FLINGO-LABEL%3E%3CLINGO-LABEL%3EPowerShell%3C%2FLINGO-LABEL%3E%3CLINGO-LABEL%3ESharePoint%20Online%3C%2FLINGO-LABEL%3E%3C%2FLINGO-LABS%3E%3CLINGO-SUB%20id%3D%22lingo-sub-16244%22%20slang%3D%22en-US%22%3ERe%3A%20Batch%20Update%20User%20Profile%20Properties%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-16244%22%20slang%3D%22en-US%22%3E%3CP%3EThanks%20for%20Sharing!%3C%2FP%3E%3C%2FLINGO-BODY%3E%3CLINGO-SUB%20id%3D%22lingo-sub-1502645%22%20slang%3D%22en-US%22%3ERe%3A%20Batch%20Update%20User%20Profile%20Properties%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-1502645%22%20slang%3D%22en-US%22%3E%3CP%3E%3CA%20href%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fuser%2Fviewprofilepage%2Fuser-id%2F4865%22%20target%3D%22_blank%22%3E%40Haniel%20Croitoru%3C%2FA%3E%26nbsp%3Bthis%20reply%20is%20a%20couple%20of%20years%20late%20but%20your%20script%20is%20still%20relevant%20-%20thanks%20for%20sharing!%20After%20some%20slight%20adjustments%20to%20fit%20the%20new%20PnP%20cmdlets%20it%20works%20like%20a%20charm.%20However%2C%20I%20am%20running%20to%20an%20issue%20and%20I%20was%20hoping%20someone%20might%20have%20the%20answer.%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EOne%20of%20the%20properties%20I%20am%20trying%20to%20update%20is%20the%20SPS-TimeZone%2C%20but%20it%20fails%20to%20accept%20the%20integer%20representing%20the%20SPS-TimeZone%20ID.%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3ESetting%20the%20Time%20Zone%20separately%20works%20well%2C%20like%20this%20for%20example%3A%3C%2FP%3E%3CPRE%20class%3D%22lia-code-sample%20language-powershell%22%3E%3CCODE%3ESet-PnPUserProfileProperty%20-Account%20%24upn%20-Property%20'SPS-TimeZone'%20-Value%2010%3C%2FCODE%3E%3C%2FPRE%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EBut%20when%20I%20do%20this%20using%20the%20script%2C%20I%20get%20the%20following%20error%3A%3C%2FP%3E%3CPRE%20class%3D%22lia-code-sample%20language-powershell%22%3E%3CCODE%3ECould%20not%20set%20SPS-TimeZone%20--%26gt%3B%203.%20%20Exception%20calling%20%22Substring%22%20with%20%222%22%20argument(s)%3A%20%22Index%20and%20length%20must%20refer%20to%20a%20location%20within%20the%20string.%0AParameter%20name%3A%20length%22%3C%2FCODE%3E%3C%2FPRE%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EAnd%20if%20I%20add%20it%20as%20a%20string%2C%20I%20get%20this%3A%3C%2FP%3E%3CPRE%20class%3D%22lia-code-sample%20language-powershell%22%3E%3CCODE%3ECould%20not%20set%20SPS-TimeZone%20--%26gt%3B%20%2245%22.%20%20Invalid%20time%20zone%20format.%20The%20value%20must%20be%20SPTimeZone%20object%2C%20a%20valid%20integer%20time%20zone%20ID%2C%20or%20a%20string%20representation%20of%20a%20valid%20integer%20time%20zone%20ID.%3C%2FCODE%3E%3C%2FPRE%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EThanks!%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3C%2FLINGO-BODY%3E
MVP

A few months back, the Patterns and Practices team at Microsoft announced a new User Profile Batch Update API.  It's great for updating large batches of properties, but only works for user profile properties, which have not been set to be editable for the end users to avoid situation where the user profile import process would override any information which end user has already updated.  This makes a lot of sense.  But what if you need to do some changes to these editable fields for a number of users?

 

To do just that, I wrote a script using some of the OfficeDev PnP PowerShell library.  In order to use it, you need to consider the following:

  • The input is a CSV file (sample attached to the post)
  • Log onto the admin site (e.g. https://tenant-admin.sharepoint.com)
  • The first row of the CSV must be the InternalName properties of the fields
  • The first column should be the Email of the user you're modifying
  • If you want to add multiple values to a field, they should be separated by a pipe (|)
  • For People Picker fields, use the AccountName (e.g. i:0#.f|membership|roby@tenant.onmicrosoft.com)
  • Since querying user profile property fields returns all the values as text, the code doesn't check for field types and tries to save the values provided as-is.  If it fails, it will report an error to the console.

 

Function PopulateUserProfiles
{
[CmdletBinding()]
	param
	(
	[Parameter(Position=1,Mandatory=$true)]
            [string] $csvFile
        )

    try
    {
	 $ctx = Get-SPOContext
    }
    catch
    {
        Write-Host "Please connect to tenant admin site before running this function."
        return
    }

     if (Test-Path $csvFile)
     {
        $UserData = Import-Csv $csvFile
	    $rows = $UserData | measure
	    $ColumnName = $UserData | get-member | ? {-not($_.Name -in @("Equals", "GetHashCode", "GetType", "ToString"))} | select "Name"

	    for ($i = 0; $i -lt $rows.Count; $i++)
	    {
            $Email = $UserData[$i].($ColumnName[0].Name)
            Write-Host "Updating data for $Email"

            for ($j = 1; $j -lt $ColumnName.Count; $j++)
		    {
                # if there are multiple values, then I separate them using the |.  However, if a person field is used, then the format is using the claims encoding which also includes
                # the |.  So first I check if it is a user account or not and act accordingly.  See http://www.wictorwilen.se/Post/How-Claims-encoding-works-in-SharePoint-2010.aspx
                # for more information about the claims format.
                # 
                
                $value =  $UserData[$i].($ColumnName[$j].Name)

                # if the first characters are i:0 or c:0, then it's a user account and I leave it as-is.
                if (($value.Substring(0,3) -eq "i:0") -or ($value.SubString(0,3) -eq "c:0"))
                {
                    Set-SPOUserProfileProperty -account $Email -PropertyName $ColumnName[$j].Name -Values $value -ErrorAction SilentlyContinue 
                }
                else
                {
                    # split the string using the | as a delimiter and load the values into the field.
                    Set-SPOUserProfileProperty -account $Email -PropertyName $ColumnName[$j].Name -Values $value.Split("|") -ErrorAction SilentlyContinue 
                }

                if ($?)
                {
                   Write-Host "  Set $($ColumnName[$j].Name) --> $($UserData[$i].($ColumnName[$j].Name))." -ForegroundColor Green
                }
                else
                {
                   Write-Host "  Could not set $($ColumnName[$j].Name) --> $($UserData[$i].($ColumnName[$j].Name)).  $($error[0].Exception.message)" -ForegroundColor Red
                }
            }
	    }
     }
}

I have tested out the code on a number scenarios that worked.  If you find any issues, please let me know and I'll try to correct them.

 

I hope you find it helpful!

2 Replies

Thanks for Sharing!

@Haniel Croitoru this reply is a couple of years late but your script is still relevant - thanks for sharing! After some slight adjustments to fit the new PnP cmdlets it works like a charm. However, I am running to an issue and I was hoping someone might have the answer.

 

One of the properties I am trying to update is the SPS-TimeZone, but it fails to accept the integer representing the SPS-TimeZone ID.

 

Setting the Time Zone separately works well, like this for example:

Set-PnPUserProfileProperty -Account $upn -Property 'SPS-TimeZone' -Value 10

 

But when I do this using the script, I get the following error:

Could not set SPS-TimeZone --> 3.  Exception calling "Substring" with "2" argument(s): "Index and length must refer to a location within the string.
Parameter name: length"

 

And if I add it as a string, I get this:

Could not set SPS-TimeZone --> "45".  Invalid time zone format. The value must be SPTimeZone object, a valid integer time zone ID, or a string representation of a valid integer time zone ID.

 

Thanks!