Helper Classes for the SharePoint Server 2007 Search Query Web Service Built Using the Microsoft .NET Framework

Summary: Learn about some helper classes built by using the Microsoft .NET Framework that make it easier to build query requests for the Microsoft Office SharePoint Server Search Query Web service and more straightforward to handle the query responses. (12 printed pages)

Callum Shillan, Microsoft Consulting Services

October 2007

Applies to: Microsoft Office SharePoint Server 2007, Microsoft .NET Framework, Microsoft Visual C#

Contents

  • Genesis of Helper Classes Project for the Query Web Service

  • Overview of the Solution

  • Classes in the MossAccess Project

  • Using the QueryRequest Class

  • Implementing the QueryRequest ToString() Method

  • Reviewing the QueryReponse Class

  • Using the MossQuery Class

  • Including the Test Harness

  • Conclusion

  • About the Author

  • Additional Resources

Download the accompanying code sample for this article from Codeplex, MossSrchWs.

Genesis of Helper Classes Project for the Query Web Service

The genesis for this article was an internal project that our team ran in the United Kingdom to expose information held in a series of our community sites maintained by our team leads and subject matter experts. To help the team responsible for the user interface, I created a few Microsoft .NET Framework classes that made it easier for them to fire queries into the Microsoft Office SharePoint Server 2007 Search Query Web service hosted on our main portal. These classes also made it more straightforward to handle the responses that the Web service returned.

By using these classes, the user interface (UI) team could focus on what they were good at: developing amazing user experiences (by using Microsoft Silverlight, in this case), but without needing to worry about tasks such as building XML query documents and invoking Web services. By using the existing Query Web service, I could also reduce the amount of my development time. After all, as with all internal projects, my existing customer commitments and engagements continued, so I needed to produce something quickly.

Why did the UI team use Silverlight? The choice was less about the cross-browser capability and more about the stunning vector-based graphics, media, and animation that enabled them to create a rich and compelling experience for our users.

As this project progressed, I also created a test harness that enabled me to drive my .NET Framework classes and see if everything worked as expected. I did not try to recreate or supersede the community test tools for the Windows SharePoint Services 3.0 and Microsoft Office SharePoint Server 2007 Search Query Web service. Instead, I tried to ensure that my classes worked as expected. In my opinion, the community tools that are available still have their place: You can use them to exercise much more of the Query Web service than you can exercise with my test harness.

Overview of the Solution

Before we drill down into the details of the various classes and their properties and methods, it is worth taking a few minutes to get the overall picture of the solution. The Query Web service uses a simple request/response mechanism. The QueryEx method accepts XML input that conforms to a certain schema to define the query request. It returns a dataset with a series of data tables that represent the response to the query. Although the QueryEx method returns a dataset, I wanted to give the developers something even easier to use: a few properties to set, and a method to invoke to get the results back in data tables.

After a few trials and errors, I created three main classes: QueryRequest, QueryResponse, and MossQuery. The QueryRequest class is used to define the query that will be used. The MossQuery object actually invokes the Query Web service, and it massages the result to build a QueryResponse object. Figure 1 shows these helper classes.

Figure 1. QueryRequest, QueryResponse, and MossQuery helper classes

QueryRequest, QueryResponse, MossQuery classes

For a while, I considered putting the PerformQuery method on the QueryRequest class. In this way, a developer could fill out various properties to define the query and then invoke that method to perform the query. In the end, I decided to use the three classes mentioned previously, because I thought using these classes mapped well to what a developer would intuitively understand: create a query request, fire it at Office SharePoint Server, and get a return result.

In Figure 1, we see a simplified version of my test harness. It includes a text box to contain the query text, a command button to perform the query, and a data grid view to display the results.

The code to create the search button is relatively straightforward: Create and set an instance of a QueryRequest object, invoke the PerformQuery method of the MossQuery class, and get a QueryResponse object in return. A data table is then bound to a data grid view to display the results. Behind the scenes, the PerformQuery object invokes the Query Web service and processes the results for us.

The following code shows the event handler that implements these steps.

private void buttonSearch_Click(object sender, EventArgs e)
{
   // Create a query request.
   QueryRequest queryRequest = new QueryRequest();

   // Set the query text.
   queryRequest.QueryText = textBoxQuery.Text;

   // Create a MossQuery object.
   MossQuery mossQuery = new MossQuery();

   // Perform a query and get the response.
   QueryResponse queryResponse = mossQuery.PerformQuery(queryRequest);

   // Display the data table response.
   if (   queryResponse.Success )
   {
      dataGridViewRelevantResults.DataSource =
                  
   queryResponse.DataTableQueryRelevantResults;
   }
}

Classes in the MossAccess Project

The MossAccess project contains all the classes that are used by the test harness. These include the following classes.

Table 1. Classes in the MossAccess project

Class Description

QueryRequest

Defines a request for injection into the Query Web service Query method and QueryEx method.

QueryResponse

Wraps a response that is returned from the Query Web service Query method and QueryEx method.

MossQuery

Invokes the Query Web service.

SearchMetadata

Wraps the response from the Query Web service GetSearchMetadata method.

The following sections describe these classes in more detail.

Using the QueryRequest Class

You use the QueryRequest class to define the query to be injected into the Query Web service. The Query Web service provides two methods to inject a query and get a result set of responses: Query and QueryEx. Query accepts a string that represents an XML document that conforms to the Microsoft.Search.Query Schema for Enterprise Search, and it returns a result set in the form of a string that represents an XML document that conforms to the Microsoft.Search.Response.Document Schema for Enterprise Search. QueryEx accepts the same input as the Query method, but it returns a result set in the form of a dataset. I chose to make use of QueryEx.

The QueryRequest class uses several properties that match the various elements and attributes defined in the Microsoft.Search.Query Schema for Enterprise Search. In addition to these, a few extra properties are used to control things such as the URL of the Query Web service and whether the query text is for a keyword search or for an MS-SQL search. You implement all of these properties as a series of private properties with associated get/set accessors.

The following code example shows the private properties of the QueryRequest class.

private Guid _queryID = Guid.Empty;
private string _queryText = string.Empty;
private string _originatorContext = string.Empty;
private int _startAt = 0;
private int _count = 0;
private List<QueryProperty> _properties = new List<QueryProperty>();
private bool _enableStemming = true;
private bool _trimDuplicates = true;
private bool _includeSpecialTermResults = true;
private bool _ignoreAllNoiseQuery = true;
private bool _includeRelevantResults = true;
private bool _implicitAndBehavior = true;
private bool _includeHighConfidenceResults = true;

private QueryType _queryType = QueryType.Keyword;
private string _target = string.Empty;
private string _language = "en-US";

You can achieve a good understanding of these private properties by referring to the explanation in Microsoft.Search.Query Schema for Enterprise Search. However, I discuss _properties in a little more detail in the following section.

_properties

We need to provide a way for our users to specify the properties to return and the properties to use for sorting the results. For example, a user might want to return the Author, Title, and Path properties, and he or she might want to sort by the Rank property and the Path property. Of course, we cannot know how many properties the user actually wants to return and which property should be used for sorting.

To get around this, we use a generic list of type QueryProperty. The following code example shows the QueryProperty class.

public enum IncludeInResults { Yes, No };
public enum SortDirection { Ascending, Descending };

public class QueryProperty
{
private string _propertyName;
private IncludeInResults _includeInResults;
private SortDirection? _direction;
private int? _sortorder;

/// <summary>
/// The name of the property.
/// </summary>
public string PropertyName
{
    get { return _propertyName; }
    set { _propertyName = value; }
}

/// <summary>
/// Whether to return the property in the results.
/// </summary>
public IncludeInResults IncludeInResults
{
    get { return _includeInResults; }
    set { _includeInResults = value; }
}

/// <summary>
/// The sort direction.
/// </summary>
public SortDirection? Direction
{
    get { return _direction; }
    set { _direction = value; }
}

/// <summary>
/// The sort order priority.
/// </summary>
public int? SortOrder
{
    get { return _sortorder; }
    set { _sortorder = value; }
}
    }

We see that a QueryProperty object has a name, an indication of whether it should be returned in the results, a sort direction (ascending or descending), and a sort order. The sort order is a number that can be used to prioritize the sort. This enables us, for example, to first sort by Author, and then by Title. Notice that SortOrder and SortDirection are nullable; they can have a value or they can be null. Using nullable properties makes development a little easier in the AddProperty method, which I describe in a following section.

Defining a generic list with these properties enables our users to add as many of the properties as they want. However, we do not offer a get/set accessor for this list. To maintain some control over it, the QueryRequest class has two public methods that are used to add items, as shown in the following code example.

/// <summary>
/// Add a property.
/// </summary>
/// <param name="name">Property name</param>
/// <param name="sortDirection">The sort direction</param>
/// <param name="sortOrder">The sort order</param>
public void AddProperty(string name)
{
    // Get a new query property.
    QueryProperty newProperty = new QueryProperty();

    // Set its name and indicate it should be included in results.
    newProperty.PropertyName = name;
    newProperty.IncludeInResults = IncludeInResults.Yes;

    // Add it to the list of properties.
    _properties.Add(newProperty);
}

/// <summary>
/// Add a property.
/// </summary>
/// <param name="name">Property name</param>
/// <param name="sortDirection">The sort direction</param>
/// <param name="sortOrder">The sort order</param>
public void AddProperty( string name, IncludeInResults includeInResults,
SortDirection sortDirection, int sortOrder )
{
    QueryProperty newProperty = new QueryProperty();

    // Get a new query property.
    QueryProperty newProperty = new QueryProperty();

    // Set its name and indicate whether it should be included in results.
    newProperty.PropertyName = name;
    newProperty.IncludeInResults = includeInResults;

    // Set the sort direction and order.
    newProperty.Direction = sortDirection;
    newProperty.SortOrder = sortOrder;

    // Add it to the list of properties.
    _properties.Add(newProperty);
}

The first of these code examples is the easiest to understand and also shows the benefit of the nullable SortOrder and Priority. The first method just uses the name of the property to be returned in the result set. We add more information to detail whether to include the property in the results by setting IncludeInResults appropriately. And this is where we see the benefit of the nullable Direction and SortOrder properties. We do not need to set some special value (for example, SortDirection.None) to indicate that no sort information is associated with this property. Instead, we simply do not assign any values to SortOrder and Priority, and we leave them as nulls. Again, this is beneficial when we build the Microsoft.Search.Query XML document. A developer can use this method to specify a property that should be included in the result set.

The second AddProperty method is more complex, because all QueryProperty properties are explicitly set. As with the first method, we simply create a new QueryProperty object, set the values, and add it to the private _properties list. A developer can use this method to specify a property that should be used to sort the results set, and it enables the developer to detail whether to include the property in the results set.

So, to return the Author, Title, and Path properties while sorting by the Rank and Path properties, the developer writes the following code.

queryRequest.AddProperty("Author");
queryRequest.AddProperty("Title");
queryRequest.AddProperty("Rank", IncludeInResults.No,
SortDirection.Ascending, 1);
queryRequest.AddProperty("Path", IncludeInResults.Yes,
SortDirection.Ascending, 2);

Implementing the QueryRequest ToString() Method

Because the Query Web service accepts a string input in the form of an XML document that conforms to the Microsoft.Search.Query Schema for Enterprise Search, we need a way to convert an instance of the QueryRequest class to such a string. To do this, we implement an override of the ToString() method.

It is probably worth explaining why I did not use the XML schema definition tool (xsd.exe) to create classes derived from the Microsoft.Search.Query Schema for Enterprise Search. After all, this tool creates classes that automatically serialize to a conformant XML document, so why go to the trouble of doing it all ourselves? The main reason is that xsd.exe creates a cascading series of classes. The users would need to create numerous classes, one for each XML element, and manually link them together. All in all, using the classes produced by xsd.exe just proved too cumbersome to use.

Now, if we build the XML document ourselves, there are two ways we can do it: We can use a string builder and manually build up the various XML elements and attributes, or we can use the XmlDocument, XmlElement, and XmlAttribute classes from the System.Xml namespace. Rather than use the string builder and possibly get my XML wrong by omitting a crucial command sequence, I think it is safer to use System.Xml.

Within the ToString method of the QueryRequest class, we create an XML document, add various elements and attributes, and then use its ToString method to return a correctly formatted string representation.

First, we create an XML query request document, and then we add a QueryPacket element and a Query element. We can do this with the following code.

// Create an XML document.
XmlDocument xmlQueryRequestDocument = new XmlDocument();

// Create the packet element.
XmlElement queryPacketElement = xmlQueryRequestDocument.CreateElement("QueryPacket",
"urn:Microsoft.Search.Query");

// Create the query element.
XmlElement queryElement = xmlQueryRequestDocument.CreateElement("Query");

// Add the query element to the query packet element.
queryPacketElement.AppendChild(queryElement);

// Add the query packet element to the query request document.
xmlQueryRequestDocument.AppendChild(queryPacketElement);

// Add the query ID element.
AddQueryIdElement(queryElement, this.QueryID);

// Add the context element.
AddContextElement(queryElement, this.QueryType, this.Language,
this.QueryText, this.OriginatorContext);

// Add the supported formats element (used only by the Query method, 
// not by the QueryEx method).
AddSupportedFormatsElement(queryElement);

// Add the range element.
AddRangeElement(queryElement, this.StartAt, this.Count);

// Add the properties element.
if (this.QueryType == QueryType.Keyword)
{
    AddPropertiesElement(queryElement, this.Properties);
}

// Add the this element.
AddBooleans(queryElement, this);

return (xmlQueryRequestDocument.OuterXml);
}

We start by creating an XmlDocument object, which has the CreateElement and CreateAttribute methods that we will use to create the XML elements and attributes. Basically, we create the appropriate entity and add it as a child in the relevant place.

I do not discuss each of the supporting methods used to build the XML document, but we will look at the AddPropertiesElement because it makes use of the nullable Direction property. First, we create two elements: Properties and SortByProperties. We then declare several XmlElement and XmlAttribute variables that we use as we build the Properties and SortByProperties data. This is shown in the following code example.

/// <summary>
/// Add the Properties element to the query element.
/// </summary>
/// <param name="queryElement">The query element</param>
/// <param name="queryProperties">The query properties element</param>
private void AddPropertiesElement(XmlElement queryElement, 
  IList<QueryProperty> queryProperties)
{
if (queryProperties != null)
{
    // Create the Properties element and the SortByProperties element.
    XmlElement propertiesElement =
      queryElement.OwnerDocument.CreateElement("Properties");
    XmlElement sortByPropertiesElement = 
      queryElement.OwnerDocument.CreateElement("SortByProperties");

    // Declare the various elements and attributes.
    XmlElement propertyElement;
    XmlAttribute nameAttribute;
    XmlElement sortByPropertyElement;
    XmlAttribute sortByPropertyNameAttribute;
    XmlAttribute sortByPropertyDirectionAttribute;
    XmlAttribute sortByPropertyOrderAttribute;

We then loop through each query property by using a foreach construct.

// Loop through each property name.
foreach (QueryProperty queryProperty in queryProperties)
{

First, we build the Property element. We create an appropriately named element and attribute, set the attribute, and add it to the element. This is shown in the following code example.

// Create the property element.
propertyElement = queryElement.OwnerDocument.CreateElement("Property");

// Create the name attribute.
nameAttribute = queryElement.OwnerDocument.CreateAttribute("name");

// Assign the name attribute.
nameAttribute.Value = queryProperty.PropertyName;

// Add the name attribute to the property element.
propertyElement.Attributes.Append(nameAttribute);

// Append the property element to the properties element.
propertiesElement.AppendChild(propertyElement);

Now, we make use of the nullable type to determine whether we have a property that should be included in a sort direction.

// Handle a direction, if given.
if (queryProperty.Direction != null)
{

If we have a property that must be included in the sort direction, we must create a sortByProperty element, create the various attributes, set their values, and add them to the sortByProperty element.

// Create a sortByProperty element.
sortByPropertyElement = queryElement.OwnerDocument.CreateElement("sortByProperty");

// Create name, direction, and order attributes.
sortByPropertyNameAttribute =
queryElement.OwnerDocument.CreateAttribute("name");
sortByPropertyDirectionAttribute =
queryElement.OwnerDocument.CreateAttribute("direction");
sortByPropertyOrderAttribute =
queryElement.OwnerDocument.CreateAttribute("order");

// Set the name, direction, and order attributes.
sortByPropertyNameAttribute.Value = queryProperty.PropertyName;
sortByPropertyDirectionAttribute.Value =
queryProperty.Direction.ToString();
sortByPropertyOrderAttribute.Value = queryProperty.SortOrder.ToString();

// Add the name, direction, and order attributes to the sortByProperty
// element.
sortByPropertyElement.Attributes.Append(sortByPropertyNameAttribute);
sortByPropertyElement.Attributes.Append(sortByPropertyDirectionAttribute);
sortByPropertyElement.Attributes.Append(sortByPropertyOrderAttribute);

// Add the sortByProperty element to the sortByProperties element.
sortByPropertiesElement.AppendChild(sortByPropertyElement);
}

When we exit the foreach loop, we add the Property and SortByProperties elements to the queryElement parameter. This is shown in the following code example.

// Append the propertiesElement.
if (propertiesElement.ChildNodes.Count > 0)
{
    queryElement.AppendChild(propertiesElement);
}

// Append the sortByProperties element, if it contains values.
if (sortByPropertiesElement.ChildNodes.Count > 0)
{
    queryElement.AppendChild(sortByPropertiesElement);
}

This completes the QueryRequest class. We have seen how the user can set various properties to define a query and how we use the System.XML namespace to create the string representation of the XML document that represents the query. Next we look at the QueryResponse class.

Reviewing the QueryReponse Class

The QueryResponse class is used for the response that we get from the Query Web service. This is a very simple class; it has a few private properties and simple get/set accessors. I do not discuss the accessors because they are trivial, but I will show the private properties and explain how they are used.

private int _startAt = 0;
private int _count = 0;
private int _totalAvailable = 0;
private bool _success = false;
private string _xmlQueryRequest = string.Empty;
private DataTable _dataTableQueryRelevantResults = null;
private DataTable _dataTableQuerySpecialTermResults = null;
private DataTable _dataTableQueryHighConfidenceResults = null;

Many results can be returned in a QueryReponse object, so the startAt, count, and totalAvailable properties let you know how far into a result set you actually are. This enables you to page through the results of a query that returned, for example, 100 rows.

The QueryRequest object simply lets you see what was fired into the Query Web service. It can be useful for debugging.

The Query Web service returns a dataset with three tables. We are providing simple access to those tables to make access easier for our users.

Now that we have addressed the QueryRequest and QueryResponse classes, all that remains is the MossQuery class.

Using the MossQuery Class

The MossQuery class is quite simple. It has a single public method: PerformQuery. This method takes a QueryRequest object as an input parameter, invokes the Query Web service, and returns a QueryResponse object, as shown in the following code example.

/// <summary>
/// Perform a search query against an Office SharePoint Server Search Query Web service.
/// </summary>
/// <param name="QueryRequest">The query request</param>
/// <returns>QueryResult</returns>
public QueryResponse PerformQuery(QueryRequest queryRequest)
{

We must first create the QueryResponse object that will hold the response to the query.

// Create the query response.
QueryResponse returnQueryResponse = new QueryResponse();

We then put the code to access the query service in a try/catch block. In this way, we can be good citizens and report whether we have any exceptions. Within the try/catch block, first we create a QueryService object, and then we set the credentials. This is shown in the following code example.

// Create a QueryService object.
QueryService.QueryService queryService = new QueryService.QueryService();

// Set authentication credentials.
NetworkCredential networkCredential =
CredentialCache.DefaultCredentials.GetCredential(new
Uri(queryService.Url), "NTLM");
queryService.Credentials = networkCredential;

The QueryService class is automatically created by the Microsoft Visual Studio Add Web Reference wizard, as shown in Figure 2.

Figure 2. Adding the QueryService class

Adding the QueryService class

Adding the network credentials is very easy; it requires just a few lines of code that use the default credentials from the credential cache.

We then override the target URL for the query service so that we can fire the query request at different places.

// Set the target, if appropriate.
if (string.IsNullOrEmpty(queryRequest.Target) == false)
{
    queryService.Url = queryRequest.Target;
}

Now we can build the string representation of the query request simply by invoking the ToString() method we examined earlier.

// Build an XML query string.
returnQueryResponse.XmlQueryRequest = queryRequest.ToString();

To execute the query and get the dataset of results is simple, as shown in the following code example.

// Execute the query and get the dataset of results.
DataSet dataSetResult = queryService.QueryEx(returnQueryResponse.XmlQueryRequest);

But now we perform some post-processing to make it easy for our users to use the QueryResponse object that we will return: We access the tables held in the results dataset and set various public properties on the QueryResponse object.

// Set the various response data tables.
returnQueryResponse.DataTableQueryRelevantResults = SetReturnDataTable(dataSetResult, "RelevantResults");
returnQueryResponse.DataTableQueryHighConfidenceResults = SetReturnDataTable(dataSetResult, "HighConfidenceResults");
returnQueryResponse.DataTableQuerySpecialTermResults = SetReturnDataTable(dataSetResult, "SpecialTermResults");

Lastly, we set a few control variables, for example, the number of records that are returned before finally returning the QueryResponse object.

// Set control variables.
SetControlData(returnQueryResponse.DataTableQueryRelevantResults, returnQueryResponse);
}

// Return the returnQueryResponse.
return (returnQueryResponse);

Well, that is all there is to it. The actual invocation of the Query Web service turns out to be very easy. We put all the effort into the QueryRequest class, and as a result, it is simple to build the input parameter to the Web service. Handling the response requires only a little post-processing to make things even less complex for the user.

Including the Test Harness

As I said earlier, I have included my test harness, not as a replacement for the community test tools for the Windows SharePoint Services 3.0 and Microsoft Office SharePoint Server 2007 Search Query Web service, but so that you can see how to use the QueryRequest, QueryResponse, and MossQuery classes.

One last thing, you need to change the YOUR_MOSS_SERVER values to the address of your Office SharePoint Server server in two places:

  • The QueryService Web reference in the MossAccess project

  • The TextboxTarget on Form1 of the MossInvoke project

Conclusion

In this article, I show how you can build three helper classes that make it easier to invoke the Microsoft Office SharePoint Server 2007 Search Query Web service. The helper classes are QueryRequest, MossQuery, and QueryResponse. The QueryRequest class is used to define the query, and the MossQuery object invokes the Query Web service and massages the result to build a QueryResponse object.

About the Author

Callum Shillan is a solution architect working for Microsoft in the United Kingdom. He has been working with Microsoft Visual C# and ASP.NET on large Internet Web sites, and with SharePoint Products and Technologies for the last few years. You can reach Callum at callums@microsoft.com.

Additional Resources

For more information, see the following resources: