I recently presented on the topic of Designing for Extensibility at a FASS (Friday Afternoon Seminar Series) at Clarity. The presentation was inspired by an article by Miguel A. Castro in CoDe Magazine. In this article, Miguel demonstrated the use of Providers, Plug-Ins, and Extensibility Modules to demonstrate best practices for building an extensible application.
While I was already familiar with the design patterns presented in the article, I thought Miguel did the best job I've seen in demonstrating how to use them. I had also previously heard Miguel discuss the topic as a guest on my buddy Craig Shoemaker's podcast - The Polymorphic Podcast.
The accompanying code is a Visual Studio 2008 project that contains everything you need to to explore Providers, Plug-Ins, and Extensibility Modules. I included all the necessary plumbing code that was excluded from the CoDe Magazine article due to space constraints, e.g. dealing with custom web.config sections.
The source code for the Visual Studio 2008 project has been RAR'd and is available here.
You can start the screencast by clicking on the image below.
P.S. It's 0 degrees right now in Chicago, and I turned off my furnace so I could record this screencast without the sound of it behind me. I hope you appreciate that I froze for you :)

UPDATE: This code is now in the Tafiti project in CodePlex and should be going into the main branch soon.
In this excellent post, my colleague Kevin Marshall build a prototype for integrating Tafiti search into SharePoint. I've been working with Kevin to add some functionality to the prototype and get it working as a tab on the Search Center on the Clarity intranet.
The first thing I wanted to do was to move away from having all the SharePoint search code in the search.aspx code-behind. I created a very simple provider to encapsulate the search functionality:
public abstract class SearchProvider : ProviderBase
{
public abstract LiveXmlSearchResults ExecuteSearch();
}
It's worth noting at this point that I didn't want to put any effort into refactoring any existing Tafiti code so it would work within the provider model. So when you see some cheesy branching logic a little further down, that's why :)
To support retrieving the specified provider from the application's web.config, I implemented SearchProviderCollection and SearchProviderSection classes to describe a collection of search providers, and a web.config section to describe the search provider.
public class SearchProviderCollection : ProviderCollection
{
public new SearchProvider this[string name]
{
get { return (SearchProvider)base[name]; }
}
public override void Add(ProviderBase provider)
{
if (provider == null)
throw new ArgumentNullException("provider");
if (!(provider is SearchProvider))
throw new ArgumentException
("Invalid provider type", "provider");
base.Add(provider);
}
}
public class SearchProviderSection : ConfigurationSection
{
[ConfigurationProperty("providers")]
public ProviderSettingsCollection Providers
{
get { return
(ProviderSettingsCollection)base["providers"];}
}
[StringValidator(MinLength = 1)]
[ConfigurationProperty("defaultProvider",
DefaultValue = "SearchProviderSharePoint")]
public string DefaultProvider
{
get { return (string)base["defaultProvider"]; }
set { base["defaultProvider"] = value; }
}
}
This allows me to configure the application directly in the web.config to use a specific search provider. This is supported by first defining a new sectionGroup:
<sectionGroup name="system.web">
<section name="searchService"
type="SearchProviderSection"
allowDefinition="MachineToApplication"
restartOnExternalChanges="true" />
</sectionGroup>
and then by specifying the search provider in a searchService element within :
<system.web>
<searchService defaultProvider="SearchProviderSharePoint">
<providers>
<add name="SearchProviderSharePoint"
type="SearchProviderSharePoint"/>
</providers>
</searchService>
Let's now take a look at the implementation of the SearchProviderSharePoint class:
public string QueryText { get; set; }
private string _queryPacket = string.Empty;
private WindowsImpersonationContext
_impersonationContext = null;
public override void Initialize(string name,
System.Collections.Specialized.NameValueCollection config)
{
// Left out some code, nothing fancy happening here
base.Initialize(name, config);
}
public SearchProviderSharePoint() {}
In an overloaded constructor, we format the QueryPacket that will be sent to the search service and do a simple check to see if the service is actually available. You would obviously want to do something a little more than simply throwing an exception, but you get the point:
public SearchProviderSharePoint(string queryText)
{
QueryText = queryText;
_queryPacket = String.Concat(
"<QueryPacket xmlns='urn:Microsoft.Search.Query'>",
"<Query>",
"<SupportedFormats>",
"<Format revision='1'>
urn:Microsoft.Search.Response.Document:Document</Format>",
"</SupportedFormats>",
"<Context>",
"<QueryText language='en-US' type='STRING'>",
QueryText,
"</QueryText>",
"</Context>",
"<Range>",
"<StartAt>1</StartAt>",
"<Count>100</Count>",
"</Range>",
"</Query>",
"</QueryPacket>");
if (IsSearchServiceAvailable() == false)
throw new Exception("Service unavailable");
}
You can add some more information to the QueryPacket, very similar to how you can tweak the search request in the object model before executing it, for example:
<EnableStemming>true</EnableStemming>
<TrimDuplicates>true</TrimDuplicates>
<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
<ImplicitAndBehavior>true</ImplicitAndBehavior>
<IncludeRelevanceResults>true</IncludeRelevanceResults>
<IncludeSpecialTermResults>true</IncludeSpecialTermResults>
<IncludeHighConfidenceResults>true</>IncludeHighConfidenceResults>
The code to check if the search service is online is:
private bool IsSearchServiceAvailable()
{
_impersonationContext = ((WindowsIdentity)
HttpContext.Current.User.Identity).Impersonate();
SPSearch.QueryService queryService =
new SPSearch.QueryService();
queryService.Credentials =
CredentialCache.DefaultNetworkCredentials;
string serviceStatus = queryService.Status();
_impersonationContext.Undo();
return serviceStatus.ToUpper() == "ONLINE" ? true : false;
}
The search is then executed and formatted into LiveXml format before being displayed in the browser:
public override LiveXmlSearchResults ExecuteSearch()
{
_impersonationContext = ((WindowsIdentity)
HttpContext.Current.User.Identity).Impersonate();
SPSearch.QueryService queryService =
new SPSearch.QueryService();
queryService.Credentials =
CredentialCache.DefaultNetworkCredentials;
DataSet queryResults =
queryService.QueryEx(_queryPacket);
_impersonationContext.Undo();
return CreateLiveXmlSearchResults(queryResults);
}
private LiveXmlSearchResults CreateLiveXmlSearchResults
(DataSet queryResults)
{
DataTable relevantResults =
queryResults.Tables["RelevantResults"];
LiveXmlSearchResults returnResults =
new LiveXmlSearchResults();
returnResults.searchresult.documentset._source =
"FEDERATOR_MONARCH";
returnResults.searchresult.documentset._count =
relevantResults.Rows.Count.ToString();
returnResults.searchresult.documentset._start = "0";
returnResults.searchresult.documentset._total =
relevantResults.Rows.Count.ToString();
LiveXmlWebResult[] results =
new LiveXmlWebResult[relevantResults.Rows.Count];
for (int i = 0; i < relevantResults.Rows.Count; i++)
{
LiveXmlWebResult webResult =
new LiveXmlWebResult();
webResult.title =
relevantResults.Rows[ i]["Title"].ToString();
webResult.desc = GetDescription(
relevantResults.Rows[ i]["Description"],
relevantResults.Rows[ i]["HitHighlightedSummary"]);
webResult.url =
relevantResults.Rows[ i]["Path"].ToString();
results[ i] = webResult;
}
if (results != null)
returnResults.searchresult.documentset.document =
(results.Length > 1)
? (object)results : (object)results[0];
return returnResults;
}
The GetDescription method is the exact same in Kevin's original post, so for the sake of brevity, I'll leave it out.
As promised, here's the cheesy part where I avoided refactoring the original code to fit the search logic into the new search provider. In the SoapSearch method in search.aspx.cs, we first load the SearchProviderSection from web.config. I basically leave everything intact if there is no provider specified in web.config. Otherwise, we just call the ExecuteSearch method of the provider to search against SharePoint.
private void SoapSearch(...
{
LiveXmlSearchResults result = null;
if (_provider == null)
{
SearchProviderSection section = (SearchProviderSection)
WebConfigurationManager.
GetSection("system.web/searchService");
_providers = new SearchProviderCollection();
ProvidersHelper.InstantiateProviders
(section.Providers, _providers, typeof(SearchProvider));
_provider = _providers[section.DefaultProvider];
}
try
{
if (_provider == null)
{
// Original LiveSearch prep code goes here - left out
}
else if (_provider is SearchProviderSharePoint)
{
SearchProviderSharePoint sharePointProvider =
new SearchProviderSharePoint(query);
result = sharePointProvider.ExecuteSearch();
}
// Continue ...
}
And finally, we want to get Tafiti set up as a tab on our portal's Search Center. I chose to simply create a new web site on my SharePoint front-end, I had to install .Net 3.5 first though for Tafiti to run.
You can create a new Search Page in your Search Center, stick a good old Page Viewer Web Part on it and point it to your Tafiti site.
The only thing that's acting strange now is actually signing in to Tafiti in order to be able to save your search results. When you register an application with Windows Live, you have to specify a url for it. In this case, I want this to be the address of the search page in SharePoint, which contains a Page Viewer web part to display the actual Tafiti web application. I'm prompted to sign in, but it doesn't look I really ever do get signed in - my shelved results aren't being saved to the database.
Windows 2003 Rights Management Services is a technology that allows you to apply usage policies that are permanently attached to content such as Office documents. For example, instead of restricting permissions on a folder on a network share, a usage policy may be applied to a Word document so that only certain people can open the document. The usage policy may also dictate that readers not be able to print the document, or copy its contents to the clipboard. The policy is attached to the document (or email) regardless of its physical location, e.g. a network share or a SharePoint document library.
We recently deployed WRMS at Clarity and realized that it can integrate nicely into SharePoint; allowing you to apply usage policies at the document library level. Microsoft provides an excellent
guide for configuring WRMS to integrate with SharePoint.
Our SharePoint and WRMS deployments at Clarity are on the same server, so that simplified things a lot. However, this is usually not the case, so it is important to point out that for the integration to work, the Windows Rights Management Client w/ SP2 needs to be installed on all SharePoint web front ends.
WRMS creates a Service Connection Point on the domain, so that all rights management enabled applications can automatically discover the WRMS instance to authenticate against. In the Central Administration site, there is a section that allows you to configure your portal to use WRMS. You can specify that SharePoint use the default WRMS instance on the domain to implement Information Rights Management features.
However, after selecting this option we got the following error:
The required Windows Rights Management client is present but could not be configured properly. IRM will not work until the client is configured properly.
It turns out that to configure the rights management client "properly", you have to trigger it by opening a document that has been restricted by WRMS. The only way we could accomplish this was by installing Word 2007 on the web front end server and opening a Word document which we had restricted using WRMS.
After that, we were able to configure Information Rights Management in the SharePoint Central Administration site. The solution is very frustrating in my opinion because you would never install Office programs like Word on a SharePoint server.
I'm going to uninstall Word 2007 from the server this week and see if the integration between WRMS and SharePoint still works. My hunch that it will because it looks like the WRMS client just needed to run that one time. I'll post my findings here in an update.