Computed profile property with BDC and Web Services

So you are importing user profiles from your user data store (Active Directory, LDAP, etc.) but you are missing a property that isn't defined in that source connection.  That property could be :

  • Something you can execute by having the username or ID of the user and then run a series of operation (it could be outside Web Services with business logic).
  • Something you could deduce one of the source connection property.  For example, you could parse the distinguishedName for an important OU name.  Arguably, this one might be better suited to be a real property in the data store but sometimes that isn't possible.
  • And there are probably other similar "executes" that could be ran to determine a user's property value like get "User's age" when you only have "Date of Birth"

 

Ideally, you would like to update these property values when you are running the normal full or incremental user profiles import that is available in the SSP.  That's where a Business Data Catalog (BDC) comes to the rescue.  You can create a web service that executes your business logic based on one parameter that is already available in your user profile and it will execute right after the source import executes.

 

Let's say we have a requirement for having a "GetUserAge()" by receiving a Date Of Birth (DOB).  You'll end up with a service that would look like this:

    1: using System;
    2: using System.Web;
    3: using System.Web.Services;
    4: using System.Web.Services.Protocols;
    5:  
    6: [WebService(Namespace = "https://tempuri.org/")]
    7: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    8: public class Service : System.Web.Services.WebService
    9: {
   10:     public Service () {
   11:     }
   12:  
   13:     [WebMethod]
   14:     public int GetUserAge(DateTime dateOfBirth) {
   15:         if (DateTime.Today < dateOfBirth)
   16:             return 0;
   17:         else
   18:         {
   19:             int age = DateTime.Today.Year - dateOfBirth.Year;
   20:             DateTime anniversary = new DateTime(DateTime.Today.Year, dateOfBirth.Month, dateOfBirth.Day);
   21:             if (anniversary > DateTime.Today)
   22:                 age -= 1;
   23:  
   24:             return age;
   25:         }
   26:     }
   27:     
   28: }

 

Then you will run the BDC Definition Editor that is available with the Office Server SDK (version 1.3 at the time of this writing) and try to make sense of how to plug this Web Method in there.  You'll soon realize that the BDC method type that you need for this Web Method is "SpecificFinder".  However, in order to have a SpecificFinder, you also need to have a Finder and an IdEnumerator.  Obviously, these 2 will not make sense from a Web Service point of view and my guess is that it was meant more like this :

  • GetCustomers (Finder)
  • GetCustomerIDs (IdEnumerator)
  • GetCustomerByID (SpecificFinder)

 

Note, for a complete How-To on how to use BDC with a Web Service, take a look at this very well explained article on MSDN : https://msdn2.microsoft.com/en-us/library/bb737887.aspx . It will show you how to use a Web Service and database from the SDK along with the BDC definition editor.

 

Since, in our case, we don't have a data store with all ages or date of births (which wouldn't make sense), but we really want that computed column!  So we create two "fake" Web Methods for the Finder and IdEnumerator and have them return an empty array of items.  The Web Service code will now look like :

    1: using System;
    2: using System.Web;
    3: using System.Web.Services;
    4: using System.Web.Services.Protocols;
    5:  
    6: [WebService(Namespace = "https://tempuri.org/")]
    7: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    8: public class Service : System.Web.Services.WebService
    9: {
   10:     public Service () {
   11:     }
   12:  
   13:     [WebMethod]
   14:     public int GetUserAge(DateTime dateOfBirth) {
   15:         if (DateTime.Today < dateOfBirth)
   16:             return 0;
   17:         else
   18:         {
   19:             int age = DateTime.Today.Year - dateOfBirth.Year;
   20:             DateTime anniversary = new DateTime(DateTime.Today.Year, dateOfBirth.Month, dateOfBirth.Day);
   21:             if (anniversary > DateTime.Today)
   22:                 age -= 1;
   23:  
   24:             return age;
   25:         }
   26:     }
   27:  
   28:     [WebMethod]
   29:     public int[] GetAllAges()
   30:     {
   31:         return new int[] { };
   32:     }
   33:  
   34:     [WebMethod]
   35:     public DateTime[] GetAllDateOfBirths()
   36:     {
   37:         return new DateTime[] { };
   38:     }
   39:     
   40: }

 

Now, the requirements in the BDC Definition Editor will be to create an Identifier (DateOfBirth) that will be the identifier for all parameters (obviously for the SpecificFinder's input parameter, AND all of return parameters).  That is how it determines what is an identifier.  Follow the MSDN article for a complete how-to with the BDC Definition Editor and how to assign identifiers.

 

Before connecting all this in SharePoint, my experience has also been better and easier when I return a "Class" instead of a single property (int).  Also, I believe it has another benefit but I'll have to run some performance tests.  What I believe is that if you have multiple properties to return, the web service will be only called once per user, not once per user per property.  Let's create a UserProperties class that returns our Age for now:

    1: using System;
    2: using System.Web;
    3: using System.Web.Services;
    4: using System.Web.Services.Protocols;
    5:  
    6: [WebService(Namespace = "https://tempuri.org/")]
    7: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    8: public class Service : System.Web.Services.WebService
    9: {
   10:     public Service () {
   11:     }
   12:  
   13:     [WebMethod]
   14:     public UserProperties GetUserAge(DateTime dateOfBirth)
   15:     {
   16:         if (DateTime.Today < dateOfBirth)
   17:             return new UserProperties(0);
   18:         else
   19:         {
   20:             UserProperties props = new UserProperties(DateTime.Today.Year - dateOfBirth.Year);
   21:             DateTime anniversary = new DateTime(DateTime.Today.Year, dateOfBirth.Month, dateOfBirth.Day);
   22:             if (anniversary > DateTime.Today)
   23:                 props.Age -= 1;
   24:  
   25:             return props;
   26:         }
   27:     }
   28:  
   29:     [WebMethod]
   30:     public UserProperties[] GetAllAges()
   31:     {
   32:         return new UserProperties[] { };
   33:     }
   34:  
   35:     [WebMethod]
   36:     public DateTime[] GetAllDateOfBirths()
   37:     {
   38:         return new DateTime[] { };
   39:     }
   40:  
   41:  
   42:  
   43:     public class UserProperties
   44:     {
   45:         public UserProperties() { }
   46:  
   47:         public UserProperties(int Age) {
   48:             this.Age = Age;
   49:         }
   50:  
   51:         private int age;
   52:         public int Age { 
   53:             get {return age;}
   54:             set { age = value; }
   55:         }
   56:     }
   57: }

 

What is left to do?  Make the connection with the import profiles:

  • Go back in the SSP and make sure your "Farm Search" account has enough rights. 
    • It needs all rights in the Business Data Catalog permissions section;
    • It needs "Manage Profiles" permissions in the User Profiles permissions section.
  • Import your BDC application (the Xml file you exported from the BDC Definition Editor)
  • In User Profiles and Properties, click on View Import Connections
    • Click on Create New Connection
    • Choose the type : Business Data Catalog
    • Enter the name for your connection
    • Click on the browse button and select your new BDC application
    • Choose the connection : Connect User Profile Store to Business Data Catalog Entity as a 1:1 mapping
    • In the dropdown, select the field that contains your value (DateOfBirth in my mock-up example)

 

Now that we have the BDC connection, we are left to create and map the properties:

  • In User Profiles and Properties, click on Add Profile Property:
    • Enter its name and display name
    • The Source Data Connection will be the display name of your BDC import connection defined in the previous steps
    • In the Data source field to map, select Age
    • Validate all other parameters

 

The next time you execute an import profile, your web service will be executed for each user and you now have a computed user profile property.  If you need to add another property that requires requesting some information on a 3rd party system, you could simply add the logic in your code and add a property in UserProperties, update your BDC application, and add the mapped property in the SSP. 

 

As for performance, I didn't do extensive testing, however, I tested with over 10,000 Active Directory accounts (which took about 3min 30sec to import).  I had 2 computed properties in a web service that was parsing a property of the Active Directory.  The BDC import took about 2 minutes to execute after the other one.  Considering this was made on a single Virtual Machine, it's probably acceptable.  Also, you have to remember that it's only ran when you do a profile import so it's likely to be off-hours.

 

Some other random notes on this subject:

  • The BDC import process executes sometime after the source import executes.  It's never long but it can take 1-2 minutes (I'll double-check but I'm assuming it's creating a Timer Job at the end of the source import and executes it as soon as the sequencer kicks in)
  • I haven't been able to pass two or more parameters to the web service.  If someone finds out that's possible, let me know.  For example, let's say you have a user profile with 2 properties that you'd really like to have as one.  The only way I've found so far was to send the username to the web service, contact the source data store (actually, you could contact the SharePoint user profiles then too depending on which is faster) with the username and get the 2 properties; to finally send them back together as one (concat).  Obviously, performance isn't as good since I already had the 2 properties to start with. 
  • If the "source" parameter (of your source connection) that is sent to the Web Service is empty, the Web Service will not be called so you cannot return a default value for when it's empty (or I haven't found the way yet).

 

Maxime