System characteristics of a well performing SharePoint Farm

Characteristics of a well-performing SharePoint Farm
Tempdb data files

  • Dedicated disks for the tempdb
  • Tempdb should be placed on RAID 10
  • The number of tempdb files should be equal the number of CPU Cores, and the tempdb data files should be set at an equal size

The size of the tempdb data files should be equal to the (Physical memory no. of processor). That’s the initial size of the tempdb data files.

DB management

  •  Enough headroom must be available for the databases and log files, plus enough capacity to keep up the requests
  • Pre grow all databases and logs if you can. Be sure to monitor the sizes so that you do not run out of disk space
  • When using SQL Server mirroring, do not store more than 50 databases on a single physical instance of SQL Server
  • Database servers should not be overloaded by using too many databases or data
  • The content databases must be limited to 200GB. Limit content databases to 200GB Indices must be Defragmented and rebuilt daily, if users can absorb the downtime required to rebuild
  • More than 25% disk space must be free.
  • Another instance of SQL must be created for data exceeding 5 TB.
  • When using SQL Server mirroring, more than 50 databases must not be stored on a single physical instance of SQL Server

Monitor the database server

Key performance counters to monitor are:

  • Network Wait Queue: at 0 or 1 for good performance
  • Average disk queue Length (latency): less than 5 ms
  • Memory used: less than 70%
  • Free disk space: more than 25%

 

Customized scheduled SharePoint Search Alerts in HTML

Customized scheduled SharePoint Search Alerts in HTML

Occasionally I get requests for customized notification about documents within a SharePoint farm. Regular Alerts and SharePoint Search Alerts work great out of the box, but sometimes users want something more such as:
– Complex criteria
– Custom sort sequence
– Custom metadata
– Customized notification frequency
– Custom message text, or subject line
– Refined layout

The solution I’ve used is to script a search query in powerShell, and load it into a formatted HTML table, and schedule it to the desired frequency. I’ll outline the framework below that is easily adapted and extended. Note I wrote this for FAST, but using SharePoint Search Query classes you can achieve similar results in regular SharePoint search.

First, let’s establish some basics about search and notification. For this solution, I only want to return documents that are up to two or three days old, so I grab the date, take two off it, and put it into a format we can use later for the query:

 

 $Mydate=get-date
$MyDate = $Mydate.AddDays(-2)
$MyDateStr = $Mydate.Year.ToString("0000") + "-" + $Mydate.Month.ToString("00")  + "-" + $Mydate.day.ToString("00")  #formatted YYYY-MM-DD

Let’s now set up for the search querty. I chose to use FQL (FAST Query Language), but you can use Keyword Search. Note SQL Search is deprecated. I chose 50 results, but you can choose whatever amount you prefer:

 

  $site = New-Object Microsoft.SharePoint.SPSite $webappurl
$vc =New-Object Microsoft.Office.Server.Search.Query.KeywordQuery $site
$vc.ResultsProvider = [Microsoft.Office.Server.Search.Query.SearchProvider]::FASTSearch
$vc.ResultTypes = [Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults
#In my case I enabled the FQL syntax and set some other parameters:
$vc.EnableFQL = $true # enable FQL
$vc.RowLimit = 50 # sets the limit of results
$vc.StartRow = 0 # 0 is the default

Now let’s make sure the query returns the fields you want. These must be Managed Properties configured followed by a Full Crawl.

  $vc.SelectProperties.Add("Company Name")
$vc.SelectProperties.Add("URL")
$vc.SelectProperties.Add("Title")
$vc.SelectProperties.Add("Filename")
$vc.SelectProperties.Add("Company ClaimNumber")
$vc.SelectProperties.Add("Company PolicyNumber")
$vc.SelectProperties.Add("Company Modified")
$vc.SelectProperties.Add("Company EffectiveYear")

Now let’s piece together the XML of the FQL. Note two strings ($q1 and $q2) are used to construct the query and put into $BigQ, with the date we formatted earlier. We’re looking for documents newer than two days ago, where a particular field CompanyClaimDocumentType equals a specific value (“Specific Value”). Then we execute the FQL:

$q1='and(filter(Company modified:range(datetime("'
      $q2='"), max, from="GT")), filter(CompanyClaimDocumentType:equals("Specific Value")))'
      $BigQ=$q1+$MyDateStr+$q2
 
$vc.QueryText = $BigQ
$results = $vc.Execute()

Now let’s convert the search results into a DataTable to make it easy to shape into an HTML table for the outbound email alert notification, we’ll define the columns, and load the values. One nice thing is to shape the link column to allow a hyperlink embedded in the table for easy user access to the documents. I also structure a special link using the DMF:// protocol supported by MacroView DMF:

$resultsTable = $results.Item([Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults)
$resultsDataTable = $resultsTable.Table
$rows = $resultsDataTable.Rows
 
 
$table = New-Object system.Data.DataTable “SearchReport”
 
$col1 = New-Object system.Data.DataColumn Title,([string])
$col2 = New-Object system.Data.DataColumn CompanyName,([string])
$col3 = New-Object system.Data.DataColumn ClaimNumber,([string])
$col4 = New-Object system.Data.DataColumn Link,([string])
$col5 = New-Object system.Data.DataColumn PolicyNumber,([string])
$col6 = New-Object system.Data.DataColumn Modified,([string])
$col7 = New-Object system.Data.DataColumn EffectiveYear,([string])
$col8 = New-Object system.Data.DataColumn FileName,([string])
$col9 = New-Object system.Data.DataColumn DMF,([string])
 
$table.columns.add($col1)
$table.columns.add($col2)
$table.columns.add($col3)
$table.columns.add($col4)
$table.columns.add($col5)
$table.columns.add($col6)
$table.columns.add($col7)
$table.columns.add($col8)
$table.columns.add($col9)
 
if ($rows.count -gt 0)
    {
    for ($i=0; $i -lt $rows.Count; $i++)
    {
    $row = $table.NewRow()
    $row.Link = "<a>'+ " Link " + "</a>";
    #$row.DMF = ($row.Link.Replace("http://","DMF://")).replace(" File Link", "SP Explorer")  Took out, doesn't appear to work quite right
    $row.Title = $rows[$i].Title;
    $row.InsuredName= $rows[$i].CompanyName;
    $row.ClaimNumber= $rows[$i].CompanyClaimNumber;
    $row.PolicyNumber= $rows[$i].CompanyPolicyNumber;
    $row.EffectiveYear= $rows[$i].CompanyEffectiveYear;
    $row.FileName= $rows[$i].FileName;
    $row.Modified= $rows[$i].Company Modified.substring(0,10);
 
    $table.Rows.Add($row)
    #("<a>" + $_.Portname.SubString(3) + "</a>"}}
    }

Now, we want to shape this into an outbound table in the email. To do that we’ll use the handy CovertTo-HTML CmdLet. Just pass in the column names you want to appear. Above I map more than we use below. I shape the borders and colors for a really professional look. However we have a sticky problem where we don’t want it to mess with the embedded HTML such as brackets around the a href for links. To solve that, I pipe the stream into a routine called Convert-HTMLEscape that I will outline shortly. Note the nice H1 header, and we’ll nicely handle single/plural number of rows in the subject line and top of the email:

$a = ""
$a = $a + "TABLE{border-width: 1px;border-style: solid;border-color:black;}"
$a = $a + "Table{background-color:#EFFFFF;border-collapse: collapse;}"
$a = $a + "TH{border-width:1px;padding:5px;border-style:solid;border-color:black;background-color:#DDDDDD}"
$a = $a + "TD{border-width:1px;padding-left:5px;padding-right:3px;border-style:solid;border-color:black;}"
$a = $a + ""
 
#$MyOutput = $rows | ConvertTo-Html Title, Author, URL, Link -body "<H1>Recent Custom Reports</H1>" -PostContent "Goodbye and thanks for all the fish"
 
if ($rows.count -eq 1)
{
    $Plural=$null;
}
else
{
    $Plural="s";
}
 
$MyOutput = $table| ConvertTo-Html Title, Link, InsuredName, ClaimNumber, PolicyNumber, EffectiveYear, Modified -head $a -body "<H1>$($rows.count) Recent Property Adjuster Report$($Plural)</H1>"  | Convert-HTMLEscape

Here’s the Convert-HTMLEscape function. It’s a little dense, but works within the pipeline and does what we need, by converting the XML equivalent of the two common XML characters right back to the correct characters, basically undoing the bit of a mess made by ConvertTo-HTML:

Function Convert-HTMLEscape {
 
# convert < and > to <> It is assumed that these will be in pairs
 
[cmdletbinding()]
 
Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)
 
Process {
foreach ($item in $text) {
    if ($item -match "<") {
        <#
          replace codes with actual symbols. This line is a shortcut to do two replacements with one line of code. The code in the first
          set of parentheses revised text with ".
        #>
        (($item.Replace("<","")).Replace(""",'"')
     }
     else {
        #otherwise just write the line to the pipeline
        $item
     }
 }
} #close process
 
} #close function

Now the easy part, let’s generate the email. Note the format sets isHTML to $true, and uses SMTP:

 

   #email setup, can move to top
#param( 
        [string] $From = "SharePoint Support@MyDomain.com"
        [string] $To = $null; #$Recipients #"joelplaut@MyDomain.com"
        [string] $Title = "Daily Report of updated custom Reports"
        #[string] $Body = "body"
    #)
    $Body = $rows | ConvertTo-Html
    $SmtpClient = New-Object System.Net.Mail.SmtpClient
    $SmtpServer = "mail.Company limited.com"
    $SmtpClient.host = $SmtpServer
 
#    $SmtpClient.Send($From,$To,$Title,$Body)
$MailMessage = New-Object system.net.mail.mailmessage
 
$mailmessage.from = $From;
foreach ($recip in $ToRecipientsArray)
{
    $mailmessage.To.add($recip)
}
foreach ($recip in $CCRecipientsArray)
{
    $mailmessage.CC.add($recip)
}
$mailmessage.Subject = $Title
$mailmessage.Body = $myoutput #"Body"
$MailMessage.set_IsBodyHtml($true)
$smtpclient.Send($mailmessage)

Now let’s put it all together. Note I always set an $env for the environment, to make it easy to test in Dev, before deploying in Production.

# SharePoint Search Alerts
Clear-Host
Write-Host "Start LC Alerts" -ForegroundColor darkblue
Add-PsSnapin Microsoft.SharePoint.PowerShell -erroraction silentlycontinue
 
$ToRecipients = "MyADGroup@MyDomain.com"
$CCRecipients = "joelplaut@MyDomain.com,Bozo@MyDomain.com"
$ToRecipientsArray = $ToRecipients.Split(",");
$CCRecipientsArray = $CCRecipients.Split(",");
 
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Administration")
 
$env="Prod"
if ($env -eq "Dev")
{
    $webappurl = "http ://devFarm/" # Replace with URL of web application that you wish to warm up
    $filterpath = "http ://devFarm/insureds"
}
else
{
    $webappurl = "http ://SharePoint/" # Replace with URL of web application that you wish to warm up
    $filterpath = "http ://SharePoint/insureds" #path for exercising previews
}
 
Function Convert-HTMLEscape {
 
<#
convert < and > to 
It is assumed that these will be in pairs
#>
 
[cmdletbinding()]
 
Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)
 
Process {
foreach ($item in $text) {
    if ($item -match "<") {
        <#
          replace codes with actual symbols
          This line is a shortcut to do two replacements
          with one line of code. The code in the first
          set of parentheses revised text with ".
        #>
        (($item.Replace("<","")).Replace(""",'"')
     }
     else {
        #otherwise just write the line to the pipeline
        $item
     }
 }
} #close process
 
} #close function
 
 
 
$Mydate=get-date
$MyDate = $Mydate.AddDays(-2)
$MyDateStr = $Mydate.Year.ToString("0000") + "-" + $Mydate.Month.ToString("00")  + "-" + $Mydate.day.ToString("00")  #formatted YYYY-MM-DD
 
$site = New-Object Microsoft.SharePoint.SPSite $webappurl
$vc =New-Object Microsoft.Office.Server.Search.Query.KeywordQuery $site
$vc.ResultsProvider = [Microsoft.Office.Server.Search.Query.SearchProvider]::FASTSearch
$vc.ResultTypes = [Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults
#In my case I enabled the FQL syntax and set some other parameters:
$vc.EnableFQL = $true # enable FQL
$vc.RowLimit = 50 # sets the limit of results
$vc.StartRow = 0 # 0 is the default
 
$vc.SelectProperties.Add("Company Name")
$vc.SelectProperties.Add("URL")
$vc.SelectProperties.Add("Title")
$vc.SelectProperties.Add("Filename")
$vc.SelectProperties.Add("Company ClaimNumber")
$vc.SelectProperties.Add("Company PolicyNumber")
$vc.SelectProperties.Add("Company Modified")
$vc.SelectProperties.Add("Company EffectiveYear")
 
 
#Query / Result
  
      $q1='and(filter(Company modified:range(datetime("'
      $q2='"), max, from="GT")), filter(Company claimdocumenttype:equals("Property Adjuster Reports")))'
      $BigQ=$q1+$MyDateStr+$q2
 
 
 
$vc.QueryText = $BigQ
$results = $vc.Execute()
#$results
 
$resultsTable = $results.Item([Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults)
$resultsDataTable = $resultsTable.Table
$rows = $resultsDataTable.Rows
 
 
$table = New-Object system.Data.DataTable “SearchReport”
 
$col1 = New-Object system.Data.DataColumn Title,([string])
$col2 = New-Object system.Data.DataColumn InsuredName,([string])
$col3 = New-Object system.Data.DataColumn ClaimNumber,([string])
$col4 = New-Object system.Data.DataColumn Link,([string])
$col5 = New-Object system.Data.DataColumn PolicyNumber,([string])
$col6 = New-Object system.Data.DataColumn Modified,([string])
$col7 = New-Object system.Data.DataColumn EffectiveYear,([string])
$col8 = New-Object system.Data.DataColumn FileName,([string])
$col9 = New-Object system.Data.DataColumn DMF,([string])
 
$table.columns.add($col1)
$table.columns.add($col2)
$table.columns.add($col3)
$table.columns.add($col4)
$table.columns.add($col5)
$table.columns.add($col6)
$table.columns.add($col7)
$table.columns.add($col8)
$table.columns.add($col9)
 
if ($rows.count -gt 0)
    {
    for ($i=0; $i -lt $rows.Count; $i++)
    {
    $row = $table.NewRow()
    $row.Link = "<a>'+ " Link " + "</a>";
    #$row.DMF = ($row.Link.Replace("http://","DMF://")).replace(" File Link", "SP Explorer")  Took out, doesn't appear to work quite right
    $row.Title = $rows[$i].Title;
    $row.InsuredName= $rows[$i].CompanyName;
    $row.ClaimNumber= $rows[$i].CompanyClaimNumber;
    $row.PolicyNumber= $rows[$i].CompanyPolicyNumber;
    $row.EffectiveYear= $rows[$i].CompanyEffectiveYear;
    $row.FileName= $rows[$i].FileName;
    $row.Modified= $rows[$i].Company Modified.substring(0,10);
 
    $table.Rows.Add($row)
    #("<a>" + $_.Portname.SubString(3) + "</a>"}}
    }
 
    $a = ""
    $a = $a + "TABLE{border-width: 1px;border-style: solid;border-color:black;}"
    $a = $a + "Table{background-color:#EFFFFF;border-collapse: collapse;}"
    $a = $a + "TH{border-width:1px;padding:5px;border-style:solid;border-color:black;background-color:#DDDDDD}"
    $a = $a + "TD{border-width:1px;padding-left:5px;padding-right:3px;border-style:solid;border-color:black;}"
    $a = $a + ""
 
    #Filename removed at My's suggestion
 
    #$MyOutput = $rows | ConvertTo-Html Title, Author, URL, Link -body "<H1>Recent Custom Reports</H1>" -PostContent "Goodbye and thanks for all the fish"
     
    if ($rows.count -eq 1)
    {
        $Plural=$null;
    }
    else
    {
        $Plural="s";
    }
 
    $MyOutput = $table| ConvertTo-Html Title, Link, InsuredName, ClaimNumber, PolicyNumber, EffectiveYear, Modified -head $a -body "<H1>$($rows.count) Recent Custom Report$($Plural)</H1>"  | Convert-HTMLEscape
    #$MyOutput > C:A.html #debug technique
    #email setup, can move to top
    #param( 
            [string] $From = "SharePoint Support@Company limited.com"
            [string] $To = $null; #$Recipients #"joelplaut@Company limited.com"
            [string] $Title = "Daily Report of updated Property Adjuster Reports"
            #[string] $Body = "body"
        #)
        $Body = $rows | ConvertTo-Html
        $SmtpClient = New-Object System.Net.Mail.SmtpClient
        $SmtpServer = "mail.Company limited.com"
        $SmtpClient.host = $SmtpServer
 
    #    $SmtpClient.Send($From,$To,$Title,$Body)
    $MailMessage = New-Object system.net.mail.mailmessage
 
    $mailmessage.from = $From;
    foreach ($recip in $ToRecipientsArray)
    {
        $mailmessage.To.add($recip)
    }
    foreach ($recip in $CCRecipientsArray)
    {
        $mailmessage.CC.add($recip)
    }
    $mailmessage.Subject = $Title
    $mailmessage.Body = $myoutput #"Body"
    $MailMessage.set_IsBodyHtml($true)
    $smtpclient.Send($mailmessage)
 
} #don't bother, no new hits

SharePoint Monitoring Metrics

SharePoint Monitoring Metrics

I pulled together some useful metrics for monitoring SharePoint environments:

Storage

  1. Server drive(s) disk space availability (15% free)
  2. Content DB size, and monthly growth rate
  3. Site Collection size and monthly growth rate (same as above, for where we have one site collection per content DB)
  4. Storage by file type (are people uploading home pictures or MP3s)
  5. MySites site size

Security

  1. Unique permission scopes per library

Usage

  1. Items per library, and monthly growth rate
  2. Most active site / least active site
  3. Libraries/Sites with no activity in 90 days
  4. Documents with over 200 Versions (considering version trimming of middle 75% of versions, if > 100 versions of a document)
  5. Duplicate Documents
  6. Users who do not create documents in 30 days (perhaps they are storing locally or on other media)
  7. Folders with over 2000 items in the root
  8. Any newly created library or site

Search

  1. Search terms resulting in no-hits
  2. Very frequent searches

System

  • Reboots
  • IIS Pool recycle frequency
  • Daily report of Unexpected events in ULS log (could be large, and not actionable)
  • RAM usage > 95%
  • Full Crawl exceeding 20 hours
  • Incremental crawl not running hourly
  • Failed Timer jobs
  • CPU Usage > 90% average over any five minute period
  • Disk queue length on SharePoint server
  • Web Page load time (can have a program pull a page every minute)

Setting the Minimum BLOB Storage Size Correctly

How to set the Minimum BLOB Storage Size

Setting the minimum storage size for BLOBs is a bit tricky.  It turns out that setting the property generally does not work, and the SPRemoteBlobStorageSettings object does not have an associated update() method.  Trying to update the container object (Content DB) doesn’t work either.  The trick is to use the associated method to set the value:

$rbs.set_MinimumBlobStorageSize(number)

Here’s a script that iterates through a set of content DBs, and sets the min blob size.  I prefer to work with lists of DBs:

 

 $DBList="Content_1,Content_12,Content_3"
$DBs=$DBList.split(",")
for ($i=0; $i -lt $DBs.count; $i++)
{
# Provide your content DB name using the name of the Content DB, or if there's only one Content DB for a web app, you can get it by identifying the web app
$cdb=Get-SPContentDatabase -identity $DBs[$i]
 
$rbs = $cdb.RemoteBlobStorageSettings
#here's what not to do. This does not work; min blob size is lost; save conflict
#$rbs.MinimumBlobStorageSize =25kb # this is the minimum size of a file to be sent to the Blob Store; anything smaller is kept in the Content DB.

#a much better approach is to use the method to set the value, this does not require an update() method. Note the SPRemoteBlobStorageSettings ($rbs here is that type) does not have an associated update() method
$rbs.set_MinimumBlobStorageSize(25*1024)
}

Here’s a script to ensure that RBS is configured correctly, and the min blob size is what you expect it to be:

 $DBList="Content_1,Content_12,Content_3"
$DBs=$DBList.split(",")

for ($i=0; $i -lt $DBs.count; $i++)
{
$cdb=Get-SPContentDatabase -identity $DBs[$i]
$rbs = $cdb.RemoteBlobStorageSettings

write-host “RBS installation for $($cdb.name) is $($rbs.Installed())”

write-host “MinBlobStorageSize for $($cdb.Name) is $($rbs.MinimumBlobStorageSize)”
}


	

Add a Taxonomy Field into Every Site Collection and to a Content Type

Add a Taxonomy Field into Every Site Collection

I recently had to add a Taxonomy Site Column into a range of Site Collections, and then add the new Site Column into a Content Type.  Lastly, I needed to update all libraries in the site collection to reflect the updated Content Type.  How might we do this?

Step by step

Let’s get a web application, and its sites.  Note that this is not the most efficient approach, as a collection of SPSites can be large.  A more efficient way could be to pass the Sites through a pipeline.

  $webApp=Get-Spwebapplication "http ://SharePoint" #your web app url
$sites=$webApp.sites;

Let’s loop through the SPSites, and get the SPWeb to work with.  The SPSite actually has no content, nor does it have Site Columns or Content Types.  The root web is where Site Columns and Content Types are managed.

  for ($i=0; $i -lt $sites.count; $i++)
{
$site= $sites[$i];
$JPweb=$site.rootweb;
}

To do anything with Managed Metadata requires opening a Taxonomy session.   This can be done directly through the Service Application.  For simplicity, I open the session at the Site level.  This code assumes only one Managed Metadata Service association (index offset of zero).  If you have more than one MMS service application, query the collection of termstores to ensure you are grabbing the correct one.  Note below there is a “group name” and “Term Set Name” required.  Adjust these to match your environment.  Lastly there’s a check below to ensure you grabbed a termset, and aren’t holding a null reference:

$taxSession = new-object Microsoft.SharePoint.Taxonomy.TaxonomySession($site, $true);
$termStore = $taxSession.TermStores[0];
$TermSet = $termStore.Groups["Your Term Group"].TermSets["TermSetName"]
 
if ($TermSet -eq $null)
{
Write-Host -ForegroundColor DarkRed "Termset does not exist"
continue;
}

Now let’s create a new field. Note we’ll need to know the display name for the field, as well as define a static (internal) name.  As with all Site Columns, I highly recommend using a static name that avoids spaces and other characters that lead to control escape sequences in the code (%20 for space, etc).  Note the termset that is “latched” to this field refers to the specific term store (it could point to any of your Managed Metadata Service Applications) and to the specific termset.  These are GUIDs.  Note that each Managed Metadata Service Application is associated with a dedicated database.  That’s where your termsets are stored.  You’ll want to select a “group” for where the site column will be displayed.  We’ll add the field, and update the SPWeb object.

 $taxonomyField = $JPweb.Fields.CreateNewField("TaxonomyFieldType", $FieldToAddDisplayName)
$taxonomyField.SspId = $termSet.TermStore.Id
$taxonomyField.TermSetId = $termSet.Id
$taxonomyField.AllowMultipleValues = $false
$taxonomyField.Group = "Site Column Group"
$taxonomyField.StaticName = $FieldToAddStaticName
$taxonomyField.ShowInEditForm = $true
$taxonomyField.ShowInNewForm = $true
$taxonomyField.Hidden = $false
$taxonomyField.Required = $false
 
$JPweb.Fields.Add($taxonomyField);
$JPweb.Update();

Now let’s grab the set of Content Types, find our target Content Type and field, and update the Content Type.  We’ll turn off ReadOnly for the Content Type, and do the following special object update to force propagation of the Content Type into all the libraries in the site collection: $ct.UpdateIncludingSealedAndReadOnly()

$cts=$JPWeb.ContentTypes
$ct=$cts["Specific Content Type"] # replace with your content type
 
$ct.set_ReadOnly($false)
$fields=$ct.fields
 
$fs=$JPWeb.Fields
$favField=$fs.get_Item($FieldToAddDisplayName)
 
if ($favField -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot find $($FieldToAdd) in web $($JPWeb.url)"
continue;
}
 
$link = new-object Microsoft.SharePoint.SPFieldLink $favField
$ct.FieldLinks.Add($link)
$ct.UpdateIncludingSealedAndReadOnly($true)
$ct.set_ReadOnly($true)

Let’s now put it all together into one neat script that includes some extra error handling:

 $FieldToAddStaticName = "InternalFieldName"
$FieldToAddDisplayName = "Field Display Name"
$webApp=Get-Spwebapplication "http ://SharePoint" #your web app url
$sites=$webApp.sites;
 
for ($i=0; $i -lt $sites.count; $i++)
{
$site= $sites[$i];
$JPweb=$site.rootweb;
 
if ($site.url -notlike "http ://SharePoint/MyPreferredPath/*")
{
continue;
}
 
$taxSession = new-object Microsoft.SharePoint.Taxonomy.TaxonomySession($site, $true);
$termStore = $taxSession.TermStores[0];
 $TermSet = $termStore.Groups["Your Term Group"].TermSets["TermSetName"]
 
if ($TermSet -eq $null)
{
Write-Host -ForegroundColor DarkRed "Termset does not exist"
continue;
}
 
$taxonomyField = $JPweb.Fields.CreateNewField("TaxonomyFieldType", $FieldToAddDisplayName)
 
$taxonomyField.SspId = $termSet.TermStore.Id
 $taxonomyField.TermSetId = $termSet.Id
 $taxonomyField.AllowMultipleValues = $false
 $taxonomyField.Group = "Site Column Group"
 $taxonomyField.StaticName = $FieldToAddStaticName
 $taxonomyField.ShowInEditForm = $true
 $taxonomyField.ShowInNewForm = $true
 $taxonomyField.Hidden = $false
 $taxonomyField.Required = $false
 
$JPweb.Fields.Add($taxonomyField);
 
$JPweb.Update();
$cts=$JPWeb.ContentTypes
$ct=$cts["Specific Content Type"] # replace with your content type
if ($ct -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot add field to Content Type in web $($JPWeb.url)"
continue;
}
 
$ct.set_ReadOnly($false)
$fields=$ct.fields
 
$fs=$JPWeb.Fields
$favField=$fs.get_Item($FieldToAddDisplayName)
 
if ($favField -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot find $($FieldToAdd) in web $($JPWeb.url)"
continue;
}
 
$link = new-object Microsoft.SharePoint.SPFieldLink $favField
 
$ct.FieldLinks.Add($link)
 
$ct.UpdateIncludingSealedAndReadOnly($true)
 
$ct.set_ReadOnly($true)
}

Creating a View in each Document Library in a Web Application

Crate a View in each Library in a Web App

My friend Rob Holmes raised an interesting challenge; deploying a new view to all libraries. Here’s the script to apply it to all libraries with Content Types enabled that are Document Libraries, across all libraries in all webs in all site collections of a web application.

Sorting/filtering entries in a view requires the use of CAML, for which I provide a few examples commented in the script. To write CAML, one needs to know the available fields. If you have a $Lib, you can dump the static or internal names for every field in the library with this command:

1
$Lib.Fields | select internalname

Here’s some CAML examples.

'<OrderBy><FieldRef Name="mvReceived_x0020_Time" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvReceived_x0020_Time" /></IsNotNull></Where>'
'<OrderBy><FieldRef Name="mvSentOn" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvReceived_x0020_Time" /></IsNotNull></Where>'
'<OrderBy><FieldRef Name="mvSentOn" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvSentOn" /></IsNotNull></Where>'

There’s a few good CAML builder tools. You can also just navigate an existing View and grab the XML from the View in SharePoint.

Here’s the script:

  Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue
$wa=get-spwebapplication http ://SharePoint
 
Write-Host "script starting $(get-date)"
 
$wa=get-spwebapplication http ://SharePoint
 
foreach ($Site in $wa.sites)
{
    if ($site.url -like $MatchStr)
    {
        $webs=$Site.AllWebs
        $webcount = $Site.AllWebs.Count
 
        for ($i=0; $i -lt $webcount; $i++)
        {
            $web=$webs[$i]
 
            Write-Host "working in $($web.url)"
 
            $lists=$web.lists;
 
              for ($k=0; $k -lt $lists.count; $k++)
              {
                    $JPLib = $lists[$k];
                         #don't bother adding a view to a hidden library
                    if ($JPLib.Hidden)
                    {
                        continue;
                    }
                         #only for libraries, not for GenericList and other types
                    if ($JPLib.BaseType -ne "DocumentLibrary")
                    {
                        continue;
                    }
                         #choose your own lib filter; this acts on every content type enabled library
                    if ($JPLib.ContentTypesEnabled)
                    {
                    write-host -f green "The Library $($JPLib.title) exists in the site $($web.url), about to tune the view"
                    try
                    {
                    $x=$JPLib.Views.get_Item("Email")
                    if ($x.id -ne $null) #prevents duplicate entries
                    {
                        $JPLib.Views.Delete($x.ID.ToString())
                    }
                    }
                    catch
                    {}
                    if ($JPLib.Views["Email"] -eq $null) #prevents duplicate entries
                       {
 #$viewQuery = ''
                     #$viewQuery = ''
                     $viewQuery = ''
 
                     $viewFields = New-Object System.Collections.Specialized.StringCollection
                     $viewFields.Add("DocIcon") &gt; $null
                     $viewFields.Add("LinkFilename") &gt;  $null
                     $viewFields.Add("Title") &gt;  $null
                     $viewFields.Add("mvTo") &gt;  $null
                     $viewFields.Add("mvFrom") &gt;  $null
                     $viewFields.Add("mvSubject") &gt;  $null
                     $viewFields.Add("mvSentOn") &gt;  $null
                     $viewFields.Add("mvAttach_x0020_Count") &gt;  $null
                #    $viewFields.Add("DocType") &gt;  $null # this is accounting specific
 
                     #RowLimit property
                     $viewRowLimit = 50
                     #Paged property
                     $viewPaged = $true
                     #DefaultView property
                     $viewDefaultView = $false
                     #$ViewQuery=$null;
                     $viewTitle="Email"
                     $newview = $JPLib.Views.Add($viewTitle, $viewFields, $viewQuery, $viewRowLimit, $viewPaged, $viewDefaultView)
                       }
    }
    }
   }
#} #foreach site
} #if $true/Siteurl is not null, if environment setup is valid
}<del datetime="2024-03-06T19:12:48+00:00">  </del>

Configuring Blob Cache correctly

Blob Cache

For snappy SharePoint performance, one great option to enable is Blob Caching. Make sure to first back up the web-config for your target web app, which you can navigate to via IIS.

I prefer to increase the max-age=”600″ to a full day max-age=”86400″. This parameter forces remote browser caching which really helps with remote users over relatively poor connections.

Set an appropriate location; I prefer to segregate by web app name. Microsoft suggests a 10GB maxSize, with 20% extra space. You can lower it, it rarely increases to that level.

Best is configuring this on a dedicated drive. Unless you have an enterprise-class SAN, dedicated spindles are the way to go.

For every cache, you’ll want to know how to flush it:

$webApp = Get-SPWebApplication "http://SharePoint"
[Microsoft.SharePoint.Publishing.PublishingCache]::FlushBlobCache($webApp)
Write-Host "Flushed the BLOB cache for:" $webApp
It’s totally safe to flush the cache at any time without disruption, and best of all, wastes no water 🙂

AD User group membership not propagating into site collections

AD User group membership propagation issue

In some rare instances, users may exist within a Site Collection that don’t receive their AD group membership updates.

I’ve traced this down to recreated AD users that have the same account name, yet a new SID. The solution is to wipe the user references from the site collection.

Be forewarned, any user permissions will be wiped as well. One more excellent reason to only use AD groups for assigning permissions in SharePoint!

You can see this internal list and even delete the user by adapting this URL:
http ://WebApp/ManagedPath/namedSiteCollection/_layouts/people.aspx?MembershipGroupId=0

Better to do it in PowerShell for speed, extensibility, consistency, and across many site collections. The trick comes down to a specific way to eliminate the user from the site collection:

1
$RootWeb.SiteUsers.Remove($MyUser)

Note trying $RootWeb.Users.Remove($MyUser) or $RootWeb.AllUsers.Remove($MyUser) will not work.

To finish it off, I prefer to re-add the user:

1
$RootWeb.EnsureUser($MyUser)

Here’s the full script, where I traverse through site collections in a Web App, filter them based on criteria (in this case the managed path), then carefully take the action on a list of users (one or more, comma separated), and output any failures along the way:

  Start-SPAssignment –Global
$UsersToWipe = "DOMAINPoorBloke"
$UsersToWipeArray = $UsersToWipe.Split(,)
 
$siteUrl = "http ://SharePoint" 
 
Write-Host "script starting $(get-date)"
 
$rootSite = New-Object Microsoft.SharePoint.SPSite($siteUrl)
$spWebApp = $rootSite.WebApplication 
foreach($site in $spWebApp.Sites)
{
 
 
 if ($site.Url -notlike "$siteurl/SpecificPath/*") 
 {
     Write-Host "Fast Skipping $($site.Url)"
 }
 else
  { 
   $rootWeb = $site.RootWeb;
 
   foreach ($MyUser in $UsersToWipeArray)
   {
        try
        {
            try
            {
                $user1 = $RootWeb.EnsureUser($MyUser)
            }
            catch
            {
                Write-Host "x1: Failed to ensure user $($MyUser) in $($Site.url)"
            }
 
            try
            {
                $RootWeb.SiteUsers.Remove($MyUser)
                $RootWeb.update()
            }
            catch
            {
                Write-Host "x2: Failed to remove $($MyUser) from all users in $($Site.url)"
            }
 
            try
            {
                $user1 = $RootWeb.EnsureUser($MyUser)
            }
            catch
            {
                Write-Host "x3: Failed to ensure user $($MyUser) in $($Site.url)"
            }
 
       }
       catch
       {
            Write-Host "x4: other failure for $($MyUser) in $($Site.url)"
       }
   }
 } #Site to process 
   
    $site.Dispose();  
 } #foreach Site
 
 
Write-Host "script finishing $(get-date)"
 
 
Stop-SPAssignment –Global

When users can’t access a taxonomy

When users cannot access a MMS taxonomy

The Managed Metadata Service is great; but what do you do when users can’t view some taxonomy entries? This occurs when the user does not have access to the HiddenTaxonomyList, native to each Site Collection.

You can view the list using SharePoint Manager from CodePlex, navigating the Object Model, or simply by modifying this URL to reflect your site collection URL:
http ://SharePoint/sites/SiteCollection/Lists/TaxonomyHiddenList/AllItems.aspx where “http ://SharePoint/sites/SiteCollection” is replaced with your site collection URL.

I recently found a situation where permissions by default were empty. Best would be to allocate all Authenticated users access. However what does one do if there are many site collections within a given Web Application? Here’s a script that will iterate through Site Collections, and grant the access:

 $WebApp = "http ://SharePoint" #replace with your own web app
$webapp = get-spwebapplication $webapp
 
function AddPerm ([Microsoft.SharePoint.SPList] $TargetObj, [string] $RoleValue, [string] $RoleGroup)
{ #SPWeb is implied and not passed as parms for efficiency!
    if ((!$RoleValue) -or (!$RoleGroup))
    {
    return; #race to be efficient on NullOp
    }
    try
    {
                $user = $SPWeb.ensureuser($RoleGroup)
                $roledef = $SPWeb.RoleDefinitions[$RoleValue]
                $roleass = New-Object Microsoft.SharePoint.SPRoleAssignment($user)
                $roleass.RoleDefinitionBindings.Add($roledef)
 
                $TargetObj.RoleAssignments.Add($roleass)  
    }
    catch
    {
    Write-Host -ForegroundColor DarkRed "ERR: Can't Assign $($RoleGroup)"
    }
}
 
for ($i=0; $i -lt $WebApp.Sites.Count; $i++)
{
    $site=$webapp.Sites[$i];
    $SPWeb=$site.rootweb;
    $list = $SPWeb.Lists["TaxonomyHiddenList"]
    addPerm $list "Read" "SharePoint NT Authenticated Users"
}

Send Email From PowerShell

Sending An Email from PowerShell

It’s easy to send an email from PowerShell, and it’s really useful for notifying yourself of the completion of a long-running script.

It’s also a way to document what ran when, if you are like me and running hundreds of scripts and need a way to organize documentation on when you run things.

Just alter the parameters below, and let ‘er rip and spam yourself!

   param(  
        [string] $From = "server@joelDomain.com",
        [string] $To = "joelplaut@joelDomain.com",
        [string] $Title = "title",
        [string] $Body = "body"
    )
    $SmtpClient = New-Object System.Net.Mail.SmtpClient
    $SmtpServer = "mail.domainserver.com"
    $SmtpClient.host = $SmtpServer
    $SmtpClient.Send($From,$To,$Title,$Body)

Newsletters