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.
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.