Extending the Query Application Block

The Query Application Block handles three essential components: Queries, Parameters and Commands and currently supports three types of query: a Data Query to any relational database supported by the DAAB, a Service Query to a WCF or Web service and a File Query specifically to an XML file. There are many other ways that data can be stored like the Windows Registry, LDAP sources, CSV files, INI files and other proprietary format flat files, that it is quite likely that the QAB may need to be extended in time. The key to extending the QAB is to create a custom command, optionally a custom parameter type, unless an existing type suffices like the generic NameValueParameter and a custom query to wrap the command and parameters.

Creating Custom Commands

Creating a custom command based around an existing base class has already been covered in detail on the Custom Commands page. However, so far we have only covered the situation where you need a one-off special condition outside of the norm. If however, you need something a bit more permanent or regular then you need to consider deriving from the Command base class, or even a higher class such as FileCommand perhaps? At the very least the Command base class handles the ParameterDictionary for you and also implements the ICommand interface albeit in abstract form. The real work for you as the extension developer is to handle the mechanics of the ExecuteForRead() and the ExecuteForWrite() methods. The following code segment shows the ICommand interface:

[C#]
public interface ICommand
{
	/// <summary>
	/// Gets the query parameters.
	/// </summary>
	/// <value>A dictionary of parameters.</value>
	ParameterDictionary Parameters
	{
		get;
	}

	/// <summary>
	/// Executes a read command and returns the results.
	/// </summary>
	/// <returns>
	/// Dataset containing the results of the read operation
	/// </returns>
	/// <remarks>
	/// Output parameters will update the parameter property values.
	/// </remarks>
	DataSet ExecuteForRead();

	/// <summary>
	/// Executes a write command and saves data to the data source.
	/// </summary>
	/// <remarks>
	/// Output parameters will update the parameter property values.
	/// </remarks>
	void ExecuteForWrite();
}


Note1: The ExecuteForRead() method must always return a DataSet, even an empty one, if necessary, in the case when no records match the query criteria. The DataSet should never be null. The ExecuteForWrite() has no return and should throw exceptions if it gets into trouble. Don't forget the ParameterDictionary is provided for all parameters needed for your command. It is the reponsibility of the Query wrapper to populate this parameter set. This dictionary is a set of IParameter objects (discussed in the next section) keyed with a string parameter name.

Note2: There is no configuration for custom commands as they are completely managed by their query wrapper. However, if you wish to allow one-off customisations of your new command type and you have created a brand new base class; then you wll need to copy one of the existing design nodes and modify the filter used in the Type Selector to enable these custom queries to be registered and ultimately linked to queries through the configuration console.

Creating Custom Parameters

For most scenarios the generic NameValueParameter is all that is required, which simply holds an object with a name, but as you can see from the DataParameter and the XmlParameter etc there are situations when something a little more sophisticated is required. When this happens then you should inherit from the abstract Parameter base class which in turn implements the IParameter interface:

[C#]
public interface IParameter
{
	/// <summary>
	/// Gets or sets the name of the parameter.
	/// </summary>
	/// <value>The name of the parameter.</value>
	/// <remarks>Exclude parameter name tokens for generic queries e.g. for SQL parameter formats
	/// like @Name the @ symbol prefix can be excluded</remarks>
	string Name
	{
		get;
		set;
	}

	/// <summary>
	/// Gets or sets the parameter value.
	/// </summary>
	/// <value>The parameter value.</value>
	object Value
	{
		get;
		set;
	}
}


Note: Parameter classes are simple data classes and require little or no processing. You will receive instances of these types as an IParameter through the ParameterDictionary so you will need to cast them in your command type to get at any specific properties other than Name and Value.

Creating Custom Queries

A Query is essentially a wrapper for a Command object and performs three fundamental functions. Firstly it instantiates the appropriate Command object or the configured one, if present, and passes control to its ExecuteForX methods, secondly it handles the command parameters validating them against configuration and finally it handles any instrumentation and logging. For your custom query, then, You need to inherit from the QueryBase base class which in turn implements the IQuery interface as follows:

[C#]
public interface IQuery
{
	/// <summary>
	/// Gets the query name.
	/// </summary>
	/// <value>The name.</value>
	string Name
	{
		get;
	}

	/// <summary>
	/// Gets the custom command.
	/// </summary>
	/// <value>The custom command.</value>
	ICustomCommand CustomCommand
	{
		get;
	}

	/// <summary>
	/// Gets the query parameter set.
	/// </summary>
	/// <value>A set of parameters.</value>
	IParameterSet ParameterSet
	{
		get;
	}

	/// <summary>
	/// Gets or sets the instrumentation provider.
	/// </summary>
	/// <value>The instrumentation provider.</value>
	QueryInstrumentationProvider InstrumentationProvider
	{
		get;
	}

	/// <summary>
	/// Executes a read command and returns the results.
	/// </summary>
	/// <returns>
	/// Dataset containing the results of the read operation
	/// </returns>
	/// <remarks>
	/// 	<para>this method requires the parameter values to be set through the properties.</para>
	/// 	<para>Output parameters will update the parameter property values.</para>
	/// </remarks>
	DataSet ExecuteForRead();

	/// <summary>
	/// Executes a read command and returns the results.
	/// </summary>
	/// <param name="parameterValues">The parameter values.</param>
	/// <returns>
	/// Dataset containing the results of the read operation
	/// </returns>
	/// <remarks>
	/// 	<para>This method takes the parameter values from the given dictionary.</para>
	/// 	<para>Output parameters will update the parameter property values.</para>
	/// </remarks>
	DataSet ExecuteForRead(IDictionary<string, object> parameterValues);

	/// <summary>
	/// Executes a write command and saves data to the data source.
	/// </summary>
	/// <remarks>
	/// 	<para>this method requires the parameter values to be set through the properties.</para>
	/// 	<para>Output parameters will update the parameter property values.</para>
	/// </remarks>
	void ExecuteForWrite();

	/// <summary>
	/// Executes a write command and saves data to the data source.
	/// </summary>
	/// <param name="parameterValues">The parameter values.</param>
	/// <remarks>
	/// 	<para>This method takes the parameter values from the given dictionary.</para>
	/// 	<para>Output parameters will update the parameter property values.</para>
	/// </remarks>
	void ExecuteForWrite(IDictionary<string, object> parameterValues);
}


Note1: If a Custom Command is present then this must be used in preference to your built-in wrapped Command. The Command property of the ICustomCommand will provide you with a reflected instance of the custom command.

Note2: The QueryFactory is responsible for building the ParameterSet from configuration. The Parameters property of this set gives you the ParameterDictionary that you need to pass to your command.

Note3: See the Instrumentation page for details on instrumentation, however, it is the responsibility of your query to maintain counters and log errors etc. The QueryBase class provides two convenient methods; UpdateReadCounters() and UpdateWriteCounters() to handle the mechanics for you. You simply need to call these within the appropriate ExecuteForX method.

Note4: The ExecuteForRead() and ExecuteForWrite() methods are intended to be wrappers only to the equivalent methods in your Command object. It is best not to put any additional processing here apart from instrumentation calls, exception handling and object instantiation (via the BuildCommand() method. See Note5). The overloaded method calls are to provide parameter value population and query execution in one step.

Note5: The QueryBase class provides one other protected abstract method; BuildCommand(). This method is intended for the instantiation of your Command object and where you will pass through the parameters and any other configured data to your Command. You then call this method in each of your ExecuteForX methods to get at your Command.

Configuring your Custom Extension

See the section on Configuration for details on how to configure a CustomQuery or a CustomParameter. Note that a Custom Command does not need to be configured unless you want to allow one-off customisations and then you will need to interface into the EntLibContrib.Query.Configuration.Design project. The important thing to note is that all the parameters that you need to pass to your custom query or parameter constructor, will be configured through the Attributes collection:

Adding Custom Attributes

Note: these custom attribute values are static. If you need different attribute value choices then you will need to implement a custom query or parameter for each attribute value choice. Note that you will need one attribute to be able to hold a ParameterSet name and perhaps one for a CustomCommand name if this is to be allowed.

Finally you must register your custom type. Click on the ellipsis button next to the TypeName property to load the type selector, you will not be able to load any assembly that does not contain the appropriate custom type:

Registering a Custom Query Type

Now you are done configuring, you should be able to execute queries using your custom query provider and custom parameter types, if specified, in exactly the same way as any other query provider.

Last edited Dec 22, 2009 at 7:46 PM by ewdev, version 12

Comments

No comments yet.