Monday, January 5, 2009

How do I manage a Farm level counter in SharePoint?

Recently, I had to add a counter into a metadata for each document added in all documents libraries in a SharePoint farm. This counter is composed of several text identifiers plus a number (a chrono). This number is incremented automatically each time a new document is added.
For instance, a generated code is 2-45-IT-TXT-0110000 and the next one will be 2-45-IT-TXT-0110001 or 2-C-IT-TXT-0110001. So the number is incremented, which wouldn’t be a big deal with the help of an Event Receiver in every document library.

But, where to store this number?

  • It must be accessible by every document library
  • It must be accessible by every site and site collection
  • It must be accessible by every web application in the farm.

So, one solution could be to store it directly in the database, but I’m not fond of creating a database and a table for one field!

Not a good idea! Can’t we store this information directly in SharePoint, at a farm level? I googled a bit (a lot :-)) and found this interesting article from Maurice Prather. And I read a little bit more with Robin Meure. Obviously I went also to msdn for the SPPersistedObject definition. So you can read all of this for understanding the mechanism. I prefer to focus this blog on how did I put everything in place? :-)

After the theory… the code!

I have created a form in the SCAW where I can set the first value of the chrono and the datacenter it belongs to. In my architecture, a datacenter is a farm.

image

The chrono value and the datacenter value are stored in a Chrono object

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using Microsoft.SharePoint.Administration;
   6:  
   7:  
   8: namespace apichot.SharePoint.Administration
   9: {
  10:     [System.Runtime.InteropServices.GuidAttribute("BECCB1FC-3B63-45b5-937C-EB5324E6D9BB")]
  11:     public class Chrono : SPPersistedObject
  12:     {
  13:         [Persisted]
  14:         private int _currentChrono = 0;
  15:  
  16:         [Persisted]
  17:         private int _dataCenter = 0;
  18:  
  19:         //Two constructors for the serialization
  20:         public Chrono()
  21:         { ;}
  22:  
  23:         public Chrono(string name, SPPersistedObject parent, Guid guid)
  24:             : base(name, parent, guid)
  25:         { ;}
  26:  
  27:         public override string DisplayName
  28:         {
  29:             get
  30:             {
  31:                 return string.Format("ReferenceCode Chrono (DC{0})",_dataCenter);
  32:             }
  33:         }
  34:  
  35:         public int CurrentChrono
  36:         {
  37:             get
  38:             {
  39:                 return _currentChrono;
  40:             }
  41:             set
  42:             {
  43:                 _currentChrono = value;
  44:             }
  45:         }
  46:         public int DataCenter
  47:         {
  48:             get
  49:             {
  50:                 return _dataCenter;
  51:             }
  52:             set
  53:             {
  54:                 _dataCenter = value;
  55:                 this.Name = string.Format("ReferenceCodeChronoDC{0}",_dataCenter);
  56:             }
  57:         }
  58:     }
  59: }

This class inherit from SPPersistedObject and properties are set as “Persisted” in order to be stored in the SPFarm.Properties property bag.

I also created a static class for managing the chrono:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using Microsoft.SharePoint.Administration;
   6: using Microsoft.SharePoint;
   7: using System.Security.Principal;
   8: using System.Configuration;
   9: using System.Threading;
  10:  
  11: namespace apichot.SharePoint.Administration
  12: {
  13:     public class ManageChrono
  14:     {
  15:         private const string CHRONO_GUID = "BECCB1FC-3B63-45b5-937C-EB5324E6D9BB";
  16:  
  17:         public static Chrono InitChrono(SPSite currentSite)
  18:         {
  19:             Chrono _chrono = null;
  20:             try
  21:             {
  22:                 if (currentSite == null)
  23:                 {
  24:                     throw new Exception("Please initialize the site before");
  25:                 }
  26:                 if (_chrono == null)
  27:                 {
  28:                     SPFarm farm = currentSite.WebApplication.Farm;
  29:  
  30:                     _chrono = farm.GetChild<Chrono>("ReferenceCodeChronoDC1");
  31:                     if (_chrono == null)
  32:                     {
  33:                         _chrono = new Chrono("ReferenceCodeChronoDC1", farm, new Guid(CHRONO_GUID));
  34:                         _chrono.Update();
  35:                     }
  36:                 }
  37:             }
  38:             catch (Exception ex)
  39:             {
  40:                 throw new SPException(ex.Message);
  41:             }
  42:             return _chrono;
  43:  
  44:         }
  45:         public static string GetChronoCode(SPSite currentSite)
  46:         {
  47:             Chrono _chrono = InitChrono(currentSite);
  48:             return formatCode(_chrono.DataCenter, _chrono.CurrentChrono);
  49:         }
  50:         public static string GetChronoIncremented(SPSite currentSite)
  51:         {
  52:             Impersonator user = null;
  53:             Chrono _chrono = null;
  54:             try
  55:             {
  56:                 user = new Impersonator(ConfigurationSettings.AppSettings["Impersonator User"], ConfigurationSettings.AppSettings["Impersonator Domain"], ConfigurationSettings.AppSettings["Impersonator Pwd"]);
  57:                 for (int i = 0; i < 5; i++)
  58:                 {
  59:                     try
  60:                     {
  61:                         user.Impersonate();
  62:                     }
  63:                     catch (Exception ex)
  64:                     {
  65:                         Thread.Sleep(100);
  66:                     }
  67:                 }
  68:                 // Thread is now impersonating
  69:                 _chrono = InitChrono(currentSite);
  70:                 
  71:                 if (_chrono != null)
  72:                 {
  73:                     _chrono.CurrentChrono++;
  74:                     _chrono.Update(true);
  75:                 }
  76:             }
  77:             catch
  78:             {
  79:                 // Prevent exceptions propagating.
  80:             }
  81:             finally
  82:             {
  83:                 // Ensure impersonation is reverted
  84:                 if (user != null)
  85:                     user.Undo();
  86:             }
  87:             return formatCode(_chrono.DataCenter, _chrono.CurrentChrono);
  88:         }
  89:         public static Chrono GetChronoObject(SPSite currentSite)
  90:         {
  91:             return InitChrono(currentSite);
  92:         }
  93:         private static string formatCode(int dc, int counter)
  94:         {
  95:             return string.Format(@"{0:00}{1:00000}", dc, counter);
  96:         }
  97:     }
  98: }

Line 17: InitChrono makes all the work. It gets the SPSite as a parameter, and from it, retrieves the SPFarm it belongs to. Then I get the chrono (if it exists) under the “ReferenceCodeChronoDC1” tag. In my case, this method is called only from the administration page. So I don’t need to take care of access rights here.

Line 50: GetChronoIncremented. This part is accessible by every user able to add a new document in a document library. And because I don’t want to give access to the SCAW to all users !-(, I impersonate the connection. I use the “Impersonator” from Jay Nathan because it works fine :-) Certainly better than SPSecurity.RunWithElevatedPrivileges ! Don’t forget that we have to read and write the Chrono object inside the SPFarm properties
Because sometimes (rarely) in my architecture the impersonation can’t be done the first time, I made this little loop (line 57)

Line 69: I get the Chrono object retrieved from the SPFarm properties

Line 73: I increment the Chrono and save it

Line 87: returns the chrono string in a good format (Datacenter + number)

From the ItemAdded event receiver in a document library I use this Chrono like this:

try
{
this._RefCode += "-" + ManageChrono.GetChronoIncremented(properties.ListItem.ParentList.ParentWeb.Site);
}
catch (Exception ex)
{
SPEventLog.Write("CHRONO: " + ex.ToString());
}

Conclusion

By using the SPPersistedObject, it is easy (:-)) to store properties accessible by all components of a SPFarm without using external tricks.

No comments: