User Profiles
Much like search, the configuration of the User Profile service application is usually a manual process tacked onto the end of a deployment. "Oh yeah...user profiles..." I've heard too many SharePoint architects mutter with about a week-and-a-half left in the project. However, unlike search, the code to automate this process isn't nearly as complicated. Rest assured, however, it's every bit as "interesting" as the rest of the code in this appendix.
The deployment steps we'll be discussing here involve only the provisioning of the user profile properties we're going to be defining in Constants. This is really the first of two things a complete deployment would accomplish; the second is the mapping of these properties to your Active Directory import. I've decided that this will be out of scope for this book, mainly because I have a policy that AD is sacred ground, and only the client's IT shamans should be touching it.
The Feature
First things first: add a feature called "DDD Profiles" to DDD.Web and give it all the same love we've been giving to the rest of them. This will be scoped to the site collection. Unlike search, we're not technically developing against the User Profile service application; we're starting with an SPSite, getting a Microsoft.SharePoint.SPServiceContext from it, and using that as an entry point into the Microsoft.Office.Server.UserProfiles APIs. So even though our Utilities' extensions methods are hanging off of SPSite, it's only a key to unlock the door to the user profile managers. And as you'll see in the upcoming code, "managers" is very much plural.
In addition to the standard site collection-scoped configuration, (refer to the "Taxonomy" section of this Appendix) we're going to be adding a feature activation dependency to Structure. One of our profile properties is going to be mapped to a taxonomy field, which requires a TermSet object, which requires the Structure feature to have provisioned it. So wire that up through the DDD Profiles feature designer, which should look like this when you're done:
With all the configuration in place for this feature, the next step is to add the feature receiver. Also, make sure that we have references to Microsoft.SharePoint.Taxonomy, Microsoft.Office.Server, and Microsoft.Office.Server.UserProfiles by way of DDD.Dependencies. Once that's all wired up, we can take a look at the code:
Code Listing 92: Profiles.EventReceiver.cs
- using Tax = Microsoft.SharePoint.Taxonomy;
- ...
- public override void FeatureActivated(SPFeatureReceiverProperties properties)
- {
- //initialization
- SPSite site = ((SPSite)properties.Feature.Parent);
- //ensure section
- int displayOrder = site.EnsureProfileSection(
- Constants.UserProfiles.Section.InternalName,
- Constants.UserProfiles.Section.DisplayName);
- //ensure about me property
- site.EnsureUserProfileProperty(
- Constants.UserProfiles.MyBio.InternalName,
- Constants.UserProfiles.MyBio.DisplayName,
- ++displayOrder,
- Constants.UserProfiles.MyBio.Length,
- Constants.UserProfiles.MyBio.Type);
- //ensure employee id property
- site.EnsureUserProfileProperty(
- Constants.UserProfiles.EmployeeId.InternalName,
- Constants.UserProfiles.EmployeeId.DisplayName,
- ++displayOrder,
- Constants.UserProfiles.EmployeeId.Length,
- Constants.UserProfiles.EmployeeId.Type);
- //ensure i like trees property
- site.EnsureUserProfileProperty(
- Constants.UserProfiles.ILikeTrees.InternalName,
- Constants.UserProfiles.ILikeTrees.DisplayName,
- ++displayOrder,
- Constants.UserProfiles.ILikeTrees.Length,
- Constants.UserProfiles.ILikeTrees.Type);
- //get taxonomy session
- Tax.TaxonomySession session = new Tax.TaxonomySession(site);
- //get group
- Tax.Group group = session.EnsureGroup(
- Constants.Taxonomy.GroupId,
- null);
- //get term set
- Tax.TermSet termSet = session.EnsureTermSet(
- group,
- Constants.Taxonomy.TermSetId,
- null,
- null);
- //ensure term set property
- site.EnsureUserProfileProperty(
- Constants.UserProfiles.TermSet.InternalName,
- Constants.UserProfiles.TermSet.DisplayName,
- ++displayOrder,
- 0,
- null,
- termSet);
- }
This logic, again following the "Create if not already there" paradigm in the context of a service application, first creates a profile section (Line #8), and then four profile properties under it (Line #'s 12, 19, 26, and 45). A confusing thing about the user profile API is that both sections and properties are represented by the same underlying object: a CoreProperty. This is an interesting implementation, since sections are fundamentally different and much "smaller" in complexity than properties. Nevertheless, ‘tis the nature of the beast.
Beyond being obnoxious, the pain of this architecture is felt when attempting to add custom properties to a custom section. If you don't use a section, your profiles end up in the out-of-the-box "Custom Properties" section. Since we have nice groupings for everything else in All Code, (site columns, content types, and taxonomy) then it shall be so for user profiles! I just didn't think it would be this hard.
The challenge presented by everything being a CoreProperty is that the object doesn't have a child collection (like terms) or a group property (like content types). It seems like CoreProperty was designed to model a property (makes sense) and then forced to also be a section. Since properties don't have children, then, technically, neither do sections.
Gander back at Line #8: the method to create a section returns an...integer? Yup. The way you group your profile properties under sections is to order your CoreProperties sequentially! So when we ensure a section, we grab its index (called DisplayOrder) and pass a prefix-plus-plused incrementation of it to all of our properties. So if the section we're creating has a DisplayOrder of n, (all properties and sections in the service application are in the same sequence) our four properties are indexed at (n+1), (n+2), (n+3), and (n+4) respectively. It follows that the next section's DisplayOrder would be (n+5) and its first property falls in at (n+6).
So pursuant to this convention, sections are essentially just headings nestled into an indexed listing of properties in numerical order. So to "assign" a property to a different section, you simply change its DisplayOrder to be one larger than its new parent. Furthermore, you can't change any of this ordering stuff via the UI; another win for code-based deployments!
Before looking at the much more interesting code in the extension methods, let's finish off the feature receiver. Line #'s 33-43 perform the DDD operations to get the term to map to our final profile property. I was lazy here, and instead of feeding these methods everything they need to ensure the group and term set, I simply take a dependency on Structure and assume they already exist. Of course, if you'd rather not deal with the activation dependency, feel free to fill out these methods proper.
Ensuring Properties
Now let's get dirty. We'll first look at EnsureUserProfileProperty, as EnsureProfileSection is essentially just a trimmed down version of it (much in the same way a section is a trimmed down version of a property). The code below demonstrates all of the various operations we'll be performing against the user profile APIs.
Code Listing 93: Utilities.cs
- using Tax = Microsoft.SharePoint.Taxonomy;
- ...
- public static CoreProperty EnsureUserProfileProperty(this SPSite site, string internalName, string displayName, int displayOrder, int length, string type, Tax.TermSet termSet = null)
- {
- //initialization
- bool propIsNew = false;
- internalName = internalName.Replace(" ", string.Empty);
- //get service context
- SPServiceContext context = SPServiceContext.GetContext(site);
- //get managers
- UserProfileConfigManager upcm = new UserProfileConfigManager(context);
- CorePropertyManager cpm = upcm.ProfilePropertyManager.GetCoreProperties();
- ProfilePropertyManager ppm = upcm.ProfilePropertyManager;
- UserProfileManager upm = new UserProfileManager(context);
- ProfileTypePropertyManager ptpm = ppm.GetProfileTypeProperties(ProfileType.User);
- ProfileSubtypePropertyManager pspm = upm.DefaultProfileSubtypeProperties;
- //get property
- CoreProperty cp = cpm.GetPropertyByName(internalName);
- if (cp == null)
- {
- //create property if it doesnt exist
- propIsNew = true;
- cp = cpm.Create(false);
- cp.Name = internalName;
- //determine type
- if (termSet != null)
- {
- //term set
- cp.Length = 3600;
- cp.Type = PropertyDataType.StringSingleValue;
- cp.TermSet = termSet;
- }
- else
- {
- //standard type
- cp.Type = type;
- if (length > 0)
- cp.Length = length;
- }
- //multivalued properties are not supported
- cp.IsMultivalued = false;
- cp.Separator = MultiValueSeparator.Unknown;
- }
- //property metadata
- cp.IsAlias = false;
- cp.IsSearchable = true;
- cp.DisplayName = displayName;
- cp.Description = string.Format("The is the {0} user profile property.", displayName);
- //save core property
- if (propIsNew)
- cpm.Add(cp);
- else
- cp.Commit();
- //property type
- ProfileTypeProperty profileTypeProp = propIsNew ? ptpm.Create(cp) : ptpm.GetPropertyByName(internalName);
- profileTypeProp.IsVisibleOnEditor = true;
- profileTypeProp.IsVisibleOnViewer = true;
- profileTypeProp.IsReplicable = true;
- //save property type
- if (propIsNew)
- ptpm.Add(profileTypeProp);
- else
- profileTypeProp.Commit();
- //property sub type
- ProfileSubtypeProperty profileSubTypeProp = propIsNew ? pspm.Create(profileTypeProp) : pspm.GetPropertyByName(internalName);
- profileSubTypeProp.PrivacyPolicy = PrivacyPolicy.OptIn;
- profileSubTypeProp.DefaultPrivacy = Privacy.Public;
- profileSubTypeProp.IsUserEditable = true;
- //save property sub type
- if (propIsNew)
- pspm.Add(profileSubTypeProp);
- else
- profileSubTypeProp.Commit();
- //add to section
- pspm.SetDisplayOrderByPropertyName(internalName, displayOrder);
- pspm.CommitDisplayOrder();
- //return
- return cp;
- }
Following DDD best practices, we have a display name and a unique identifier (internal name) for our properties just like our site columns. Unfortunately, the API doesn't let us use guids here; internal names (as strings) will have to be good enough. This is why Line #7 makes sure there are not spaces in the internal name, as that's the convention SharePoint requires.
Next, on Line #9, we new up an SPServiceContext from the extended SPSite, and from there, on Line #'s 11-16, we get the aforementioned managers. These managers are our programmatic entry point to the user profile service application. I'm guessing we're not operating against a service app explicitly because some of these manager classes have been around since the Shared Services Provider (the predecessor to service applications) days back in 2007. Let's break them down by line numbers:
- UserProfileConfigManager: this is the queen bee in the user profile API hive; her main job is simply to lay more manager eggs. We use this class to beget the next two.
- CorePropertyManager: this allows us create and save the underlying CoreProperty that represents our sections and properties, and deals with the "main" pieces of metadata for them. Line #'s 23-48 take care to only populate the "readonly" properties when we're allowed to: at creation time. (If you then edit these properties via the UI, you'll see them disabled.) These include the type, name, and term set (if applicable).
- ProfilePropertyManager: this is just a pass-through to the ProfileTypePropertyManager below.
- UserProfileManager: normally, I use this object when I'm querying the user profiles as a data source; it is generally considered read only for metadata. It's only job here to uncover the ProfileSubtypePropertyManager at the end of this list.
- ProfileTypePropertyManager: this creates and updates ProfileTypeProperty objects, which represent other metadata about the property (see Line #'s 55-58)
- ProfileSubtypePropertyManager: this is the same as the ProfilePropertyManager, just a different section of metadata. This is on Line #'s 66-68. It's also used to set the ordering for the section assignment on Line #'s 74 and 75.
Other than instantiating the right managers in the right order to do the right CRUD operations, there are some other interesting things going on here. First, let's talk taxonomy on Line #'s 29-31. Normally I'd split this into two different methods for metadata and non-metadata based properties (as I do for the various site column extensions) but that would necessitate massive method signatures passing these heavy managers around. So instead, if you give EnsureUserProfileProperty a termset, it ignores the "length" and "type" parameters and sets the corresponding CoreProperty members as required by the API.
Check out the persistence logic in Line #'s 50-53, 60-63, and 70-73. These use the "propIsNew" Boolean to track whether the object in question is being saved for the first time. If it's indeed new, it's "saved" by adding it to its manager's collection; updates simply need to be "committed." An important note here is that calling Commit will save a new property object, but it will not be available to the preceding manager.
This comes into play on Line #'s 75 and 76. The logic works fine on updates, because the ProfileSubtypePropertyManager will be able to find the underlying CoreProperty by internal name since it's in the database. However, on creates, it can't. Adding it to the collection the first time makes the begotten managers "aware" of the new property even if it hasn't yet been persisted to the database when the manager was instantiated.
The last thing to point out about this method is that I'm taking a lot of liberties with the property metadata. For example, everything is public, editable, and searchable. Also, I chose to lazily not deal with multi-valued properties. If your requirements are more dynamic, you can consider an approach that has the feature receiver itself instantiate all the managers, the extension method only handing persistence and returning the CoreProperty, and then setting the metadata more "globally."
Ensuring Sections
Let's now take a look at the sections. This whole mess with the ordering is completely optional as it adds no functionality; it's just prettier. As I said, sections, like properties, are just CoreProperties with no metadata. And yet it still requires instantiating five managers to get it done! Below is EnsureProfileSection which does the work for us:
Code Listing 94: Utilities.cs
- public static int EnsureProfileSection(this SPSite site, string internalName, string displayName)
- {
- //initialization
- internalName = internalName.Replace(" ", string.Empty);
- //get service context
- SPServiceContext context = SPServiceContext.GetContext(site);
- //get managers
- UserProfileConfigManager upcm = new UserProfileConfigManager(context);
- CorePropertyManager cpm = upcm.ProfilePropertyManager.GetCoreProperties();
- ProfileTypePropertyManager ptpm = upcm.ProfilePropertyManager.GetProfileTypeProperties(ProfileType.User);
- UserProfileManager upm = new UserProfileManager(context);
- ProfileSubtypePropertyManager pspm = upm.DefaultProfileSubtypeProperties;
- //get section
- CoreProperty cp = cpm.GetSectionByName(internalName);
- if (cp == null)
- {
- //create section core property
- cp = cpm.Create(true);
- cp.Name = internalName;
- cp.DisplayName = displayName;
- cp.Description = string.Format("The is the {0} user profile section.", displayName);
- cpm.Add(cp);
- //create section profile type
- ProfileTypeProperty profileTypeProp = ptpm.Create(cp);
- ptpm.Add(profileTypeProp);
- //create section profile sub type
- ProfileSubtypeProperty profileSubTypeProp = pspm.Create(profileTypeProp);
- pspm.Add(profileSubTypeProp);
- }
- else
- {
- //update section
- cp.DisplayName = displayName;
- cp.Description = string.Format("The is the {0} user profile section.", displayName);
- cp.Commit();
- }
- //get display order
- return pspm.GetSectionByName(internalName).DisplayOrder;
- }
This method starts out the same as its big brother: trim spaces from the internal name of the section, get a service context, and instantiate all the managers. In the "create" case of the ensuring if statement on Line #15, notice that we build-and-save a ProfileTypeProperty and a ProfileSubtypeProperty for the section, but don't do anything with these objects.
Even though they aren't represented at all in the UI screens, we need to make sure they exist to support the section; the "Manage User Properties" screen will throw an error if they don't. You can see why this is the case in Line #38: The ProfileSubtypePropertyManager won't be able to locate the section and return its DisplayOrder unless you've done your diligence in Line #'s 24-28.
The Script
The final step is to add this to our deployment scripts and push it out. Since we have an activation dependency on another site collection-scoped feature, Structure, I like to just nuzzle this guy into FeatureActivator.ps1 after it. (Feel free to separate this into its own PowerShell if you prefer.) The bolded lines below show to additions to the file:
Code Listing 95: FeatureActivator.ps1
- ...
- #get feature guids
- [System.Reflection.Assembly]::LoadWithPartialName("DDD.Common");
- Write-Host;
- ...
- $profiles = [DDD.Common.Constants+Features]::Profiles;
- $structure = [DDD.Common.Constants+Features]::Structure;
- ...
- #activate structure (on the site collection)
- Write-Host;
- Write-Host ("Activating Structure (on the site collection)...") -ForegroundColor Magenta;
- $script = Join-Path $path "\FeatureEnsureer.ps1";
- .$script -Url $url -Id $structure -Scope "site";
- ...
- #activate user profiles (on the site collection)
- Write-Host;
- Write-Host ("Activating Profiles (on the site collection)...") -ForegroundColor Magenta;
- $script = Join-Path $path "\FeatureEnsureer.ps1";
- .$script -Url $url -Id $profiles -Scope "site";
- ...
Now, the first time you run DoEverythinger.ps1 with your shiny new user profile bits included, you might see the following error in the code:
Even though this is the only account on my machine (making it intrinsically a farm administrator and an everything else administrator) we still need to grant ourselves explicit permissions in the user profile service application. Navigate to the "Manage service applications" in central admin, highlight the "User Profile Service Application" row, and click "Administrators" in the ribbon. See the screen show below:
I'm not trying to be a baby about this, as there's nothing wrong with demanding tight permissions, especially in an environment like SharePoint. But that said, code running as farm admin should be able to do whatever it wants, no questions asked. Would the president of the United States have to show ID when viewing an R-rated movie?
That does it for user profile deployment! Just like search, user profiles are usually something that's configured by hand in every environment. Too many times I've seen solutions claiming to be "one hundred percent automated" that had manual steps. When I asked how this was the case, the answer was basically "Well, you know, come on...user profiles aren't deployed." Fail. Follow Deployment Driven Design for all your projects and implement your SharePoint ones with All Code: make your one hundred percent automated deployments indeed automated one hundred percent!