If you have perused even a few of the previous chapters, it should be fairly obvious that all application blocks are driven by their configuration information. The configuration information for an application block defines how the block and its providers will get the information they need to initialize, validate, and determine if any cross-block relationships exist. This provides several benefits.
However, there is one detriment to being so configuration driven. It is sometimes more difficult to debug an issue in an application if it involves the way an application block or provider is configured. Developers are used to stepping through code if an issue occurs in an application, but unfortunately there is no way to step through the configuration data that drives the runtime behavior for an application block or provider. That is one reason why it is so important to use a tool like the Enterprise Library Configuration Tool for configuring an application block and its providers. While it will not catch every possible issue that can occur, it can catch many of them. Specifying how the runtime configuration information for an application block will be represented entails designing and developing the set of objects that are needed to contain the configuration settings for that application block. For example, for the Data Access Application Block to function properly, the block needs to obtain information about the connection string to use for connecting to a database and the type of database that an application is configured to use (e.g., SQL Server, Oracle, or DB2). Therefore, it contains runtime configuration objects for representing the connection string and database type. The Configuration Application Block reads settings from storage and returns the objects to the application block. Because all of the StorageProviders that ship with the Configuration Application Block require an object to be XML-serializable, the runtime configuration objects for a custom application block must also be XML-serializable. Therefore, in all of the runtime configuration objects in Enterprise Library and many of the code samples that you will see in the rest of this chapter, you will notice that the properties exposed by an object will be attributed with certain XML attributes like XmlIgnore, XmlAttribute, and XmlArray. These attributes allow the XmlSerializer to recognize how the values for these properties should be serialized and deserialized. For example, XmlIgnore indicates that the XmlSerializer should not attempt to serialize the value returned by that property. XmlAttribute signifies that the XmlSerializer should write the values out as an XML attribute and should read the values for this property out from this XML attribute when deserializing the object. Figure 9.4 depicts the runtime configuration objects that are needed for the Data Mapping Application Block and the DataSetMappingProvider that ships with it. I won't provide the code for every class that is shown in Figure 9.4 because much of the code that is created for each class is similar in its intent; the classes primarily differ only in the specific properties that they expose. I will, however, provide code samples for the classes where there is a significant difference in the way the class needs to be created or the purpose it serves. This should provide enough detailed information for you to be able to design and develop the objects that represent the runtime configuration for your own application blocks. All of the code for this application block is on this book's Web site if you want to review the code for all of the configuration objects. Figure 9.4. Runtime Configuration Object Graph for the Data Mapping Application BlockThe first runtime configuration class that needs to be created for an application block is the one that will hold the configuration information for the entire block. Typically this class is named according to the name of the application block and suffixed with Settings (e.g., DatabaseSettings, ExceptionHandlingSettings, LoggingSettings, etc.). For the Data Mapping Application Block I have named this class DataMappingSettings. In this class, the section name that identifies the section that the metaconfiguration data for this block will exist in must be specified. The Configuration Application Block looks for the metaconfiguration data in this section in the application's domain configuration file (i.e., app.config or web.config) and also uses the section name as the default file name for the configuration file (when using the XmlFileStorageProvider). Because the section name in the application's domain configuration file must be unique, the string that represents this application block must also be unique among all application blocks. Listing 9.4 shows the code for the DataMappingSettings class. This class contains properties that return the name of the default DataMappingProvider that has been set through configuration, as well as a property that returns a collection of objects that contain the configuration data for the DataMappingProviders that have been configured for an application. Additionally, the section name is set to DataMappings. This means that the Configuration Application Block will read and write the metaconfiguration data for this application block in a section named DataMappings, and if the XmlFileStorageProvider is used to store the configuration data for this application block, that configuration data will be serialized to a file named DataMappings.config by default. Listing 9.4. The DataMappingSettings Class
The rest of the runtime configuration objects that need to be created are to represent the configuration data for the DataSetMappingProvider and not the core functionality for the Data Mapping Application Block. Often the configuration data for a provider can be represented by an object or two. However, the configuration data for the DataSetMappingProvider is a bit more complex. At the lowest level I need to map DataFields in a Data-Table to the parameters for stored procedures. So, I need an object to represent that mapping. I named this object CommandParameterMapping, and it holds the state for the name of the parameter and the source column (i.e., the name of the DataField) for the parameter. If the source column is left blank, the Data Mapping Application Block will assume that there is no mapping and that the value for the parameter will be set at runtime. This is to allow for non-DataSet parameters. (For information on the UpdateDataSet method, see the section Updating Data in Chapter 3.) Furthermore, it is perfectly normal that different stored procedures may need to be called depending on the type of change that occurs in a Data-Table in a DataSet. One stored procedure will generally need to be called to retrieve the data that is used to populate the DataTable; a different stored procedure is typically used when data is deleted from the table. Still another stored procedure is called when inserting or updating records in the DataTable. Often the same stored procedure will be called to handle both inserts and updates by determining if a database record already exists for the data that was modified in the DataTable: if a record does not exist, then the stored procedure performs an insert; otherwise, it updates the record that already exists. I needed to create a configuration object that couples the name of the stored procedure that needs to be called for a specific operation for a DataTable with the collection of parameter mappings for that stored procedure. This class is named CommandMapping. The CommandMapping contains a collection of CommandParameterMappings, the command text (i.e., the stored procedure name), the command timeout, and an enumeration that represents the type of operation that it represents. The enumeration, CommandStatementType, contains values for Select, Insert, Update, Delete, and InsertUpdate.InsertUpdate is used to specify that the Data Mapping Application Block should call the same stored procedure for both inserts and updates that occur in the DataTable. This way, administrators who configure the Data Mapping Application Block do not need to replicate the same set of configuration data for two different CommandMappings just because they differ in the type of operations that they handle. I have also created a specialized CommandMapping, named SelectCommandMapping, which derives from the CommandMapping class. This class has its CommandStatementType preset to Select. This type of command is used a bit differently than the other types because, in addition to setting a SelectCommand for a DataTable, an application may need to set multiple SelectCommands for the DataSet itself. One of the SelectCommands must be configured as the DefaultSelectCommand that will be used to retrieve the data for the entire DataSet; however, other SelectCommands can be used to return DataReaders and to call ExecuteNonQuery commands. Since many DataTables can exist in one DataSet, I also needed to be able to signify the CommandMappings that are configured for each DataTable in the DataSet. The DataTableMapping object holds the state for the name of the DataTable and the CommandMappings for each of the types of operations that can occur for a DataTable. Lastly, I needed to create the DataSetMapping to hold the collection of DataTableMappings and a collection of SelectCommandMappings. In addition to these collections, the DataSetMapping also keeps state about the name of the DataSet, the default SelectCommand (if one exists) for populating all the DataTables in the DataSet, the DatabaseInstance that is used to access the Database object from the Data Access Application Block, and a transaction's IsolationLevel so operations on the entire DataSet can be wrapped in the scope of a single database transaction. Because some of this same functionality must exist whether or not DataSets are used to map the relational database information to an object representation, many of its properties are actually defined in the DataMapping base class from which the DataSetMapping class derives. As mentioned previously, I am not going to show all the code for all of these classes. Instead, the code for just the CommandMapping configuration object is provided in Listing 9.5. The code for the other configuration objects is very similar, differing only in the types of properties they expose. Listing 9.5. The CommandMapping Class
Once the core functionality, providers, factories, and runtime configuration objects have been created for an application block and its provider implementations, the application block can be used in an application. However, to do so requires manually creating the configuration data for the application block and manually modifying it whenever a change occurs. Because of the deep configuration hierarchy that is needed by this application block, manually creating and modifying the configuration data are exercises that are very prone to error. Besides that, while an application block is certainly usable without any design-time capabilities, it just doesn't feel like an application block that is on par with the application blocks that ship with Enterprise Library. To make it an application block that can be used in the Enterprise Library Configuration Tool as easily as the application blocks that ship with Enterprise Library, the design-time features must be added to the application block. |