Anda di halaman 1dari 58

Enterprise Library Extensibility Hands-on Labs - Welcome!

Learning Map
1. Custom exception handler ( 1 hr 15 mins) 2. Custom logging trace listener (1 hr) 3. Custom configuration source (registry-based) (30 mins)

Authors
Grigori Melnik, Microsoft Corporation Chris Tavares, Microsoft Corporation Daniel Piessens, Zywave Inc.

System Requirements
Enterprise Library 5.0 / Unity 2.0 installed SQL Express 2008 installed Visual Studio 2010 Pro (or better) installed

Update 1/12/2011 Preview

EntLibExtensibility HOLs Page 1

Copyright
This document is provided as-is. Information and views expressed in this document, including URL and other Internet Web site references, may change without notice. You bear the risk of using it. Some examples depicted herein are provided for illustration only and are fictitious. No real association or connection is intended or should be inferred. This document does not provide you with any legal rights to any intellectual property in any Microsoft product. You may copy and use this document for your internal, reference purposes. You may modify this document for your internal, reference purposes only. 2011 Microsoft. All rights reserved.

Microsoft, Windows, MSDN, SQL Server, IntelliSense, Visual C# and Visual Studio are trademarks of the Microsoft group of companies. All other trademarks are property of their respective owners.

EntLibExtensibility HOLs Page 2

Lab 1: Custom Exception Handler

In this series of exercises, we'll be working through an example of building a custom ExceptionHandler to plug into the Exception Handling Application Block. The handler we'll be building will be addressing a common scenario when doing data access: isolating your code from the specifics of the database. ADO.NET drivers throw driver-specific exceptions: SqlException, OracleException, ODBCException, etc., when something goes wrong. Usually these exceptions aren't something you want getting out to your main application logic. Typically, higher levels of your code neither know nor care about the details of the database, or even if a database is being used at all.
Out of the box, the Exception block provides the Wrap and Replace handlers to help with this scenario. But they're very coarse grained - they only operate based on exception type. SqlException objects, for example, hold a large amount of information about what went wrong, and that information can be used to translate the database specific exception into something more generic, but still detailed. It is useful for the higher-level code to know the difference between "TransactionDeadlockException" and "ConnectionTimedOutException".

The handler we'll be building will demonstrate this sort of logic - it'll take a SqlException object, and based on the ErrorNumber property will create a new exception object of a user-configured type.

This hands-on lab includes the following exercises: Exercise 1: Get the handler working with the bare minimum required to get it loading in the configuration tool.
Exercise 2: Provide a custom configuration experience in the configuration tool.

Exercise 3: Polish the configuration experience to take advantage of things like type pickers, help popups, etc.
Exercise 4: Provide an extension to the fluent configuration builder. Each lab exercise has a "Begin" and "End" directory. For each exercise, open the solution under "Begin" and follow the instructions. If you get stuck, you can look at the code in the End directory to see a working solution.

When completing an exercise, do not continue working on the solution you have open. Instead, close your current solution and open the solution in the "Begin" folder for the next exercise. Each starter project in the exercises includes additional code (often tests) that are not in the "End" folder from the previous exercise. This new code is necessary to complete the exercises. This lab assumes familiarity with Enterprise Library, the Exception Handling Block, and C# (including features in C# 3.0). If you require a refresher, please refer to the main set of Enterprise Library Hands-On Labs, especially the one dedicated to the Exception Handling block. Additional learning resource is the Developers Guide which can be found at http://go.microsoft.com/fwlink/?LinkId=188937

EntLibExtensibility HOLs Page 3

Exercise 1 - Building the Handler


Introduction
In this exercise, we'll be building out the exception handler, and getting it to work with Enterprise Library configuration. Estimated Time to Complete: 10 minutes

Handler Specification
The exception handler we're building will have the following inputs: errorRange: A range of SQL error numbers. If the actual error number received in the SqlException is in this range, the handler will execute, otherwise it will do nothing. wrapExceptionType: The type of exception to create if the handler fires. exceptionMessage: An optional string, if specified this will become the message in the new exception. If not specified, the new exception's message will be copied from the SqlException. shieldSqlException: An optional bool, if true, the inner exception of the new exception will be null. If false or unspecified, the inner exception of the new exception will be set to the SqlException.
Exercise Instructions: 1. Open the solution file

Open the solution file at <lab root>\CustomExceptionHandlers\Ex01\Begin \CustomExceptionHandlers.sln in Visual Studio 2010. Note that the solution includes a skeleton of the exception handler, some utility code, and unit tests. The solution does not currently compile. Upon completion of the exercise, the included tests will pass. The project already has the correct Entlib references set up. In addition, the helper classes NumericRange and NumericRangeTypeConverter are included to deal with specifying error ranges. 2. Add interface and fields Open the file SqlExceptionTranslatorHandler.cs. Inside, you'll see the start of the exception handler code, plus a series of // TODO: comments (this will appear in the VS task list).
First, to be used as an exception handler, a class must implement the IExceptionHandler interface from the Microsoft.Practices.EnterpriseLibrary.ExceptionHandling namespace. Add this interface to the class declaration. Next, add fields for each of the required inputs listed above. You can use the NumericRange class included in this project for the errorNumberRange field. Also add a constructor that sets each of these fields.

Resulting code should look like this:

EntLibExtensibility HOLs Page 4

public class SqlExceptionTranslatorHandler : IExceptionHandler { private readonly NumericRange errorNumberRange; private readonly Type wrapExceptionType; private readonly string exceptionMessage; private readonly bool shieldSqlException; public SqlExceptionTranslatorHandler(NumericRange errorNumberRange, Type wrapExceptionType, string exceptionMessage, bool shieldSqlException) { this.errorNumberRange = errorNumberRange; this.wrapExceptionType = wrapExceptionType; this.exceptionMessage = exceptionMessage; this.shieldSqlException = shieldSqlException; } 3. Implement the handler method We need an implementation of the IExceptionHandler interface. Luckily, this interface only has one method: Exception HandleException(Exception exception, Guid handlingInstanceId)
This method should do whatever processing it needs to, and return the exception that should be passed through to the rest of the handler chain. So, if replacing the exception, it should return the new exception, otherwise it should return the original exception to let the rest of the handlers get a crack at it.

The logic of this method should be as follows: First, check if the exception is actually a SqlException. If it isn't do nothing. If it is, check if the SqlException.Number is in the specified error range (NumericRange has an Includes() method to do this check). If not, do nothing. If in range, construct a new instance of the configured exception type (Activator.CreateInstance() is a useful call here), setting the exception message and inner exception based on the parameters passed in. Finally, return the new exception, or the original one if no processing was performed.
Your resulting code should look like this: public Exception HandleException(Exception exception, Guid handlingInstanceId) { Exception result = exception; var sqlException = exception as SqlException; if (sqlException != null) { if (errorNumberRange.Includes(sqlException.Number)) { string newExceptionMessage = exceptionMessage ??
EntLibExtensibility HOLs Page 5

string newExceptionMessage = exceptionMessage ?? sqlException.Message; Exception innerException = shieldSqlException ? null : sqlException; result = (Exception) Activator.CreateInstance( wrapExceptionType, newExceptionMessage, innerException); } } return result; } 4. Hook up to Entlib Configuration We will be using the bare bones configuration support in Enterprise Library for this exercise. Each Entlib application block allows for a "custom" handler or provider; this allows you to specify an arbitrary set of name/value pairs in configuration. You get no special design time support, but it is less work to implement the handler. To do this, there are two things required. First, add the ConfigurationElementType attribute to your handler class. This attribute takes the type of the configuration element. For a custom handler, that type is CustomerHandlerData. The result should look like this: [ConfigurationElementType(typeof(CustomHandlerData))] public class SqlExceptionTranslatorHandler : IExceptionHandler { Second, a new public constructor must be added to the handler. This must take a single parameter of type System.Collection.Specialized.NameValueCollection. The name/value pairs from configuration will be passed to the constructor through this collection. The SqlExceptionTranslatorHandler class already includes helper methods at the bottom of the class (inside the "Helper methods for using NameValueCollection" region) to pull out the appropriate values from the collection. Write the constructor using these helper methods, and pass the resulting values to the constructor you've already written.
Note: This is a general good practice to follow when writing custom handlers: define a constructor that takes the specific types you need, then with the config constructor, pull the data out of the collection and pass it to the other constructor to do the actual work. You get better separation of responsibilities that way, which results in more maintainable code.

The resulting constructor should look like this: public SqlExceptionTranslatorHandler(NameValueCollection attributes) : this(GetRange(attributes), GetExceptionType(attributes), GetExceptionMessage(attributes), GetShieldSqlException(attributes)) { } 5. Test the results Look at the two test fixtures. The SqlExceptionTranslatorHandlerFixture tests the handler directly,
EntLibExtensibility HOLs Page 6

Look at the two test fixtures. The SqlExceptionTranslatorHandlerFixture tests the handler directly, while the SqlExceptionTranslatorHandlerConfigurationFixture tests the handler while going through configuration. This latter test uses the fluent configuration builder rather than an explicit configuration file, but the operation is the same. Run the tests. They should all pass. Note: if you don't have the SqlExpress installed, in the data configuration builder section of the test fixture SqlExceptionTranslatorHandlerConfigurationFixture.cs, you can change the DataSouce to point to another database of your choice. If you need help, or would like to see a working version, look at the solution in the "End" directory for this exercise.

EntLibExtensibility HOLs Page 7

Exercise 2 - Plugging into the Config Tool

Introduction
In the previous exercise, we built the basic handler. But the configuration tool did not have any explicit support for this handler; instead, it just provides bare name-value pairs, and it's up to the user to know what those string keys need to be, and there's no help if they're wrong. In this exercise, we'll start building a custom design-time experience so that the tool can start giving better support. Estimated Time to Complete: 20 minutes

Exercise Instructions
1. Create the ConfigurationSet
Since we're going to be plugging into the configuration tool, it'll help to have an easy way to run it. The configuration tool requires that any additional providers be in the same directory as the tool itself. That means putting the code for our DLL into the Entlib binary directory. In order to avoid having to copy our DLL into Program Files all the time, the lab folder includes a copy of the Enterprise Library 5.0 binaries, at <lab root>\Reference Assemblies\Entlib 5, including the configuration tool itself. We'll be copying our binary here, and running this copy of the config tool, to avoid corrupting the default copy of Entlib 5 on our machines. In the <lab root>\Reference Assemblies\Entlib 5, there is a Powershell script, CreateConfigurationSet.ps1. Open this folder in Windows Explorer and run this script. This will add registry entries so that Visual Studio will find a new place to look for the Entlib config tool. Note: If you have never used Powershell, by default script execution is disabled. You will need to enable script execution first. To do so, run a Powershell prompt as Administrator and enter this command: set-executionpolicy RemoteSigned This will enable powershell script execution on this machine. See http://technet.microsoft.com/enus/library/cc764242.aspx for background on what this command does.

2. Open the solution

Open the solution file <lab root>\CustomExceptionHandlers\Ex02\Begin \CustomExceptionHandlers.sln.


3. Set the solution's configuration set We're going to set up Visual Studio so that when you right click on an App.config file and select "Edit Enterprise Library Configuration" it will run the copy in Reference Assemblies\Entlib 5 instead of the one installed in Program Files. To do so, select the Solution node at the top of the VS Solution Explorer, then open the Properties pane (hit the F4 key). Do not right click and choose "Properties", as that brings up a separate, different set of properties. In the properties pane, you'll see an entry labeled "Enterprise Library 5 Assembly Set". Change this setting using the dropdown menu and select "EntlibExtensibilityLabs".
EntLibExtensibility HOLs Page 8

setting using the dropdown menu and select "EntlibExtensibilityLabs".

Note: This is a per-solution setting, so changing it for this project will not affect any other projects on your system.

4. Look at the current configuration experience In the SqlExceptionMapping.Tests project, there is an App.config file with the SqlExceptionTranslatorHandler already configured. Right click on this App.config file, select "Edit Enterprise Library 5 Configuration", and drill down to the "Exception Translator" handler. It should look like this:

This is the experience you get in the config tool when using the CustomHandlerData class as was done in the first exercise. 5. Point Enterprise Library at a custom configuration element

The first thing to do is tell Enterprise Library that our handler will be using a custom-written configuration element. To do this, open the SqlExceptionTranslatorHandler.cs file. Change the ConfigurationElementType attribute on the class from typeof(CustomHandlerData) to typeof(SqlExceptionTranslatorData).
6. Create default constructor for the custom handler data class Open the SqlExceptionMapping\Configuration\SqlExceptionTranslatorData.cs file. This file already contains the bare bones for a configuration element. For exception handlers, the base class is ExceptionHandlerData. Each block has a similar base "Data" class that is used for each of the
EntLibExtensibility HOLs Page 9

ExceptionHandlerData. Each block has a similar base "Data" class that is used for each of the provider types you can write (the list of all extension points can be found at http://msdn.microsoft.com/en-us/library/ff664458(PandP.50).aspx) Add a default constructor to the class. It takes no parameters, and must call its base class constructor, passing the type of the handler that it configures (SqlExceptionTranslatorHandler). The resulting constructor should look like this: public SqlExceptionTranslatorData() : base(typeof(SqlExceptionTranslatorHandler)) { } 7. Add configuration property for ErrorRange The first property we care about is the errorRange. This is internally represented as a NumericRange object. However, System.Configuration doesnt do well with complex custom types as configuration properties. To work around this limitation, Enterprise Library configuration elements typically use two properties. The first one is of a simple type (typically a string), and is what is presented to the configuration system. The second one is not marked as a configuration property, but does the conversion from the string to the actual type we want. First, add a property of type string named ErrorRangeText. It should get and set its value from the configuration dictionary provided by the base class (accessed as "base[keyName]"). There are string constants for each of our configuration property names already defined at the top of the file; use the errorRangePropertyName constant as the key. When writing the property setter, do a quick validation check by attempting to convert the string to a NumericRange object using the object in the rangeConverter field. Once you've defined the property, add the ConfigurationProperty attribute, passing the string name of the property (this becomes the attribute name in the XML file) and "IsRequired = true". This property definition should look like this: [ConfigurationProperty(errorRangePropertyName, IsRequired = true)] public string ErrorRangeText { get { return (string) base[errorRangePropertyName]; } set { // Do error check on format rangeConverter.ConvertFromString(value); base[errorRangePropertyName] = value; } }

Once that is done, define a second property of type NumericRange named ErrorRange. The getter should grab the value out of the configuration dictionary and convert it to a NumericRange object, using the rangeConverter again. The setter should convert the value to a string, and then set it in the configuration dictionary. This property is not marked with the ConfigurationProperty attribute. This property definition should look like this: public NumericRange ErrorRange
EntLibExtensibility HOLs Page 10

public NumericRange ErrorRange { get { return (NumericRange) rangeConverter.ConvertFromString( ErrorRangeText); } set { ErrorRangeText = rangeConverter.ConvertToString(value); } } 8. Add configuration property for WrapExceptionType The second required property is WrapExceptionType. Again, this property is a complex type (type System.Type) so we create two properties. The actual configuration property will be of type string again, and should be named WrapExceptionTypeName. The second property is of type Type, and is named WrapExceptionType. Use the exceptionTypeConverter field to do the string <-> type conversions as needed. The resulting code should be: [ConfigurationProperty(wrapExceptionTypePropertyName, IsRequired = true)] public string WrapExceptionTypeName { get { return (string) base[wrapExceptionTypePropertyName]; } set { // error check on format exceptionTypeConverter.ConvertFromString(value); base[wrapExceptionTypePropertyName] = value; } } public Type WrapExceptionType { get { return (Type) exceptionTypeConverter.ConvertFrom(WrapExceptionTypeName); } set { WrapExceptionTypeName = exceptionTypeConverter.ConvertToString(value); } } 9. Add configuration properties for ExceptionMessage and ShieldSqlException Our final two properties are ExceptionMessage and ShieldSqlException. Since these two properties are simple types (string and bool respectively) we dont need to use the two-property solution that we used previously. Create the two configuration properties. These are both optional, so mark them IsRequired = false, and provide default values ("" for the message, and false for ShieldSqlException). The resulting code should be: [ConfigurationProperty(exceptionMessagePropertyName, IsRequired = false, DefaultValue = "")] public string ExceptionMessage { get { return (string) base[exceptionMessagePropertyName]; } set { base[exceptionMessagePropertyName] = value; } }
EntLibExtensibility HOLs Page 11

} [ConfigurationProperty(shieldSqlExceptionPropertyName, IsRequired = false, DefaultValue = false)] public bool ShieldSqlException { get { return (bool) base[shieldSqlExceptionPropertyName]; } set { base[shieldSqlExceptionPropertyName] = value; } } 10. Add constructor that sets properties By convention, Entlib configuration elements always have at least two constructors. The first is the default constructor. The second takes the various property values, as well as the name (required by the configuration system). Define a second constructor. It should take these parameters: string name NumericRange errorRange Type wrapExceptionType string exceptionMessage bool shieldSqlException

Pass the name and typeof(SqlExceptionTranslatorHandler) to the base class. Use the rest of the parameters to initialize the object's properties.
The resulting code should be: public SqlExceptionTranslatorData(string name, NumericRange errorRange, Type wrapExceptionType, string exceptionMessage, bool shieldSqlException) : base(name, typeof(SqlExceptionTranslatorHandler)) { ErrorRange = errorRange; WrapExceptionType = wrapExceptionType; ExceptionMessage = exceptionMessage; ShieldSqlException = shieldSqlException; } 11. Override the GetRegistrations() method The GetRegistrations() method is the core of the Entlib 5 configuration system. This is where you tell the system which objects to create, and what parameters they need. A TypeRegistration object is initialized with a lambda expression. That lambda expression should new up the underlying object, using the values from the configuration element as constructor parameters or property values. This lambda expression is not executed; instead, the underlying system picks it apart and and turns it into the appropriate DI container registrations to do the same thing. As a result, you should limit yourself to simple constructor calls and property sets (using the object initializer syntax). In addition to the constructor expression, the TypeRegistration object has two properties of it's own: Name and Lifetime. Name is the name used to identify this instance; almost every configurable thing in Entlib has a name. Lifetime has two choices - Transient or Singleton. Transient items are created anew each time they are requested, while Singleton objects are created once and then shared.
EntLibExtensibility HOLs Page 12

anew each time they are requested, while Singleton objects are created once and then shared. Exception handlers (and some other handler types) have a slight wrinkle around naming. Handlers are not referenced by the user directly by name. It's also fairly common to have different policies that have handlers with the same name. As such, just passing the Name from the configuration element directly will result in problems. To avert this, the base class provides a BuildName() method, and the GetRegistrations() method takes a namePrefix parameter. Call BuildName(), passing the namePrefix, to create the name for the type registration. Create a new TypeRegistration object. TypeRegistration is a generic class. In this case, we're registering IExceptionHandlers (the base class for all exception handlers) so use IExceptionHandler as the type parameter. As a constructor parameter, build a lambda expression that takes no parameters and news up a SqlExceptionTranslatorHandler, passing the values from the configuration element as the constructor parameters for SqlExcpetionTranslatorHandler. Set the Name property of the TypeRegistration object to BuildName(namePrefix). Set the LifetimeProperty to TypeRegistrationLifetime.Transient.

GetRegistrations() returns IEnumerable<TypeRegistration>, so you need to return a sequence, not a single value (more complex handlers may need to register multiple objects in the container). The easiest way to do this is to yield return your TypeRegistration object.
The resulting method should look like this: public override IEnumerable<TypeRegistration> GetRegistrations( string namePrefix) { yield return new TypeRegistration<IExceptionHandler>( () => new SqlExceptionTranslatorHandler(ErrorRange, WrapExceptionType, ExceptionMessage, ShieldSqlException)) { Name = BuildName(namePrefix), Lifetime = TypeRegistrationLifetime.Transient }; } 12. Test and verify the results Compile the code. Run the unit tests to be sure the runtime function of the configuration element is working correctly. Next, copy the SqlExceptionMapping.DLL file into the <lab root>\Reference Assemblies\Entlib 5 directory. If you get a "file in use" error, close any Entlib config tool you might currently be running. The right click on the App.config file in the test project and select "Edit Enterprise Library V5 Configuration". Expand the Exception Handling Setting node. Right click on the "All Exceptions" exception type and choose the "Add Handlers" method. You should see a new entry in the list of handlers.

EntLibExtensibility HOLs Page 13

Expand one of the "Exception Translator" handler nodes. Instead of the simple key-value pair UI you saw at the start of the lab, there should now be a much more specific UI with the explicit fields we want.

EntLibExtensibility HOLs Page 14

Exercise 3 - Polishing the Config Experience

Introduction
Our handler is now easier to use in the configuration tool, but the defaults still leave something to be desired. By default, the names shown are the class or property names, there are no help tips, and there's no special UI support (for example, no way to bring up the type picker). In this exercise you'll fix this and polish up the config tool experience.
Estimated Time to Complete: 10 minutes

Exercise Instructions
1. Open the solution Open the solution file <lab root>\CustomExceptionHandlers\Ex03\Begin\CustomExceptionHandlers.sln. Double check the Entlib assembly set property on the solution and make sure it's set to SummitWorkshop2010.
2. Add a display name and description to the data class

The strings displayed by the config tool are driven by attributes on the data class. First, we'll fix up the title that's displayed on the "Add Handler" menu.
Add two attributes to the class, DisplayName and Description. They should have these values: Attribute Value

DisplayName
Description

Sql Exception Translation Handler


A handler that will tra nslate SqlExceptions in to other exception typ es if the SQL error number is in the given range

3. Add DisplayNames and Descriptions to each configuration property The DisplayName and Description attributes are also used for the labels and help tooltips on each of the configuration properties. Add these attributes to each property with the values shown. Property ErrorRangeText WrapExceptionTypeName ExceptionMessage DisplayName Error Range Description The range of values that will cau se the exception to be translated

Wrapping Excepti The type to wrap the SqlExceptio on Type n with Exception Message Exception message for wrapped exception. If empty, the wrappe d exception's message will be us ed
If true, the Sql Exception will be shielded from the catch, and not passed up. If false, the SqlExcep tion will be passed as the inner e xception in the wrapped excepti on

ShieldSqlException

Shield Sql Exception

4. Add validation on the Error Range property

EntLibExtensibility HOLs Page 15

The ErrorRangeText property has to be formatted specifically, but there's no validation in the configuration system. A configuration validator class and attribute has been included in the project.
Add the [ErrorRangeTextConfigurationValidator] to the ErrorRangeText property. 5. Add a type picker to the WrapExceptionTypeName property

Entering type names is tedious and easy to get wrong. The Entlib config tool includes a type picker, so let's hook it up to this property. This involves two attributes.
The first is the Editor attribute. This specifies which editor to use inside the config tool for this field. Add this attribute to the WrapExceptionTypeName property:

[Editor(CommonDesignTime.EditorTypes.TypeSelector, CommonDesignTime.EditorTypes.UITypeEditor)]
The second attribute to add is the BaseType attribute. This controls what kind of types the type picker will show. Without it, it'll show every type. Since we specifically want to deal with exceptions, we will restrict the type picker to only show types deriving from System.Exception. Add the BaseType attribute, as shown, to the WrapExceptionTypeName property. [BaseType(typeof(Exception), TypeSelectorIncludes.BaseType)] 6. Test the results

Compile the project, copy the SqlExceptionMapping.DLL file to the <lab root>\Reference Assemblies\Entlib 5 directory, and then open the configuration file in the Entlib config tool.
The "Add Handlers" dialog should be showing the correct display name and descriptive text.

The properties for the exception handlers should now show the display names and descriptions in tooltips.

EntLibExtensibility HOLs Page 16

If an invalid value is entered for the error range, a validation error will show in the tool.

And the Wrapping Exception Type field now has a browse button that will, if pressed, bring up a type picker that lets you choose an exception type.

EntLibExtensibility HOLs Page 17

Exercise 4 - Extending the Fluent Configuration Builder

Introduction
Enterprise Library 5 introduced a new way of doing programmatic configuration - the fluent configuration builders. This approach lets you write code to configure Entlib that is fairly straightforward, easy to read later, and leverages IntelliSense in Visual Studio to guide you in writing your configuration code.
The fluent configuration interface is extensible. In this exercise, we'll be adding fluent configuration support for our new handler.

Estimated Time To Completion: 30 minutes

Designing the Fluent Interface


The first step is to design the grammar of the language you want to implement in your fluent interface. The exception block's fluent configuration start like this:
var builder = new ConfigurationSourceBuilder(); builder.ConfigureExceptionHandling() .GivenPolicyWithName("Translation Policy") .ForExceptionType<SqlException>() ... Specify handler here ... This is the point we want to plug into the configuration - where we specify which handler to use. Here's an example of the full grammar we're going to implement: var builder = new ConfigurationSourceBuilder(); builder.ConfigureExceptionHandling() .GivenPolicyWithName("Translation Policy") .ForExceptionType<SqlException>() .TranslateSqlExceptions() .ToType<NullReferenceException>() .IfInRange(new NumericRange(49000, 51000)) .WithMessage("Something went wrong") .AndShieldOriginalException() .ThenThrowNewException(); (This example is included in the SqlExceptionTranslatorFluentConfigurationFixture.cs file in the test project.) When building these fluent interfaces, the approach taken in Entlib is to represent each step of the "sentence" as a separate interface. This allows us to force users to specify things in a specific order, guarantee they only happen once, and to control which statements are optional and which are required. It also allows for easy composition and combination later. In our grammar, ToType is required and must occur first. The IfInRange must be next. Afterwards, you can specify the message, but it's optional, as is the "AndShieldOriginalException." In addition, we'll be
EntLibExtensibility HOLs Page 18

can specify the message, but it's optional, as is the "AndShieldOriginalException." In addition, we'll be adding a couple overloads that are not shown in the example.

Exercise Instructions
1. Open the solution Open the solution file <lab root>\CustomExceptionHandlers\Ex04\Begin \CustomExceptionHandlers.sln. Under the SqlExceptionMapping project, open the Configuration folder. Create a new folder named "Fluent" here. This is where you will be adding all the code for this exercise. 2. Create the interface for the ToType step The first step in our grammar is the "ToType()" method. We actually want two overloads. The first one takes a Type object. The second overload takes as a generic type parameter the type to translate to. Our interfaces are all going to be named "ISqlTranslation<stepname>". So our first interface will be ISqlTranslationToType. Create the ISqlTranslationToType interface. It should have one method, the ToType method that takes a Type object as a parameter. It should return the next interface in our grammar, ISqlTranslationRange. Next, create a static class (it's ok to put it in the same file with the interface). Implement the generic overload of ToType as an extension method to ISqlTranslationToType, calling the nongeneric overload (Implementing the generic methods in terms of the non-generic one is a general good practice). The resulting code should look like this: public interface ISqlTranslationToType { ISqlTranslationRange ToType(Type translatedType); }

public static class SqlTranslationToTypeExtensions { public static ISqlTranslationRange ToType<T>( this ISqlTranslationToType context) where T : Exception { return context.ToType(typeof (T)); } }
3. Create the interface for the Range step The next part in our grammar is the "IfInRange()" method. Create the ISqlTranslationRange interface. The interface should have an IfInRange() method that takes a NumericRange object. Also create an extension method in a static class that takes a string and converts it to a NumericRange, passing it the other overload.

EntLibExtensibility HOLs Page 19

The next stage after this one lets you specify either an exception message OR just straight to the exception shielding flag, so the return type should be an interface that includes both, named ISqlTranslationWithMessageOrShielding. The resulting code should look like this: public interface ISqlTranslationRange { ISqlTranslationWithMessageOrShielding IfInRange(NumericRange range); } public static class SqlTranslationRangeExtensions { public static ISqlTranslationWithMessageOrShielding IfInRange( this ISqlTranslationRange context, string range) { return context.IfInRange((NumericRange) (new NumericRangeTypeConverter().ConvertFromString(range))); } } 4. Create the interface for the Message step

Next, we need an interface for our "WithMessage()" method. It should take a string, and return the ISqlTranslationWithShielding type.
The resulting code should look like this: public interface ISqlTranslationWithMessage { ISqlTranslationWithShielding WithMessage(string message); }

5. Create the interface for the WithShielding step For the final statement in our grammar, implement the ISqlTranslationWithShielding interface. It should have two methods:
AndShieldOriginalException(), taking no parameters ShieldOriginalException(), taking a boolean.

Since this statement is the end of our grammar, we need to "return" back to the higher level of the fluent interface. This is done by returning the IExceptionConfigurationForExceptionTypeOrPostHandling type. Also, since this statement is optional, the interface should also inherit from IExceptionConfigurationForExceptionTypeOrPostHandling. The resulting code should look like this: public interface ISqlTranslationWithShielding : IExceptionConfigurationForExceptionTypeOrPostHandling { IExceptionConfigurationForExceptionTypeOrPostHandling AndShieldOriginalException();

EntLibExtensibility HOLs Page 20

IExceptionConfigurationForExceptionTypeOrPostHandling ShieldOriginalException(bool shouldShield); } 6. Create combined interface to handle optional steps After specifying the range, our grammar allows the user to give the message, or skip it completely and specify the shielding setting, or skip them both. We currently represent the two options as two separate interfaces. We'll add another interface that aggregates these two steps together. Create a new interface named ISqlTranslationWithMessageOrShielding. It should inherit from both ISqlTranslationWithMessage and ISqlTranslationWithShielding. It has not methods of its own, it exists only to combine the two types. The resulting code should look like this: public interface ISqlTranslationWithMessageOrShielding : ISqlTranslationWithMessage, ISqlTranslationWithShielding { } 7. Create a class to implement our fluent interface

The goal of all these interfaces is to create an instance of SqlExceptionTranslatorData, configured as the user requested. Now we'll be adding a type that actually implements these interfaces.
Create a class named SqlTrnaslationConfigurationSourceBuilder. It should inherit from ExceptionHandlerConfigurationExtension, and implement the ISqlTranslationToType, ISqlTranslationRange, and ISqlTranslationWithMessageOrShielding interfaces. Add a field of type SqlExceptionTranslatorData named translatorHandlerData. Add a public constructor that takes one parameter of type IExceptionConfigurationAddExceptionHandlers named context. Pass context to the base class constructor. In the body of the constructor, create an instance of SqlExceptionTranslatorData and store it in the translatorHandlerData field. Be sure to set the Name property of data class to something guaranteed to be unique (a string plus Guid works well here). Once you've done that, call the base class's CurrentExceptionTypeData.ExceptionHandlers.Add() method, passing your translatorHandlerData field's value.

The resulting code should look like this: public class SqlTranslationConfigurationSourceBuilder : ExceptionHandlerConfigurationExtension, ISqlTranslationToType, ISqlTranslationRange, ISqlTranslationWithMessageOrShielding { private readonly SqlExceptionTranslatorData translatorHandlerData;
public SqlTranslationConfigurationSourceBuilder( IExceptionConfigurationAddExceptionHandlers context)
EntLibExtensibility HOLs Page 21

IExceptionConfigurationAddExceptionHandlers context) : base(context) { translatorHandlerData = new SqlExceptionTranslatorData() { Name = "SqlTranslation" + Guid.NewGuid() }; base.CurrentExceptionTypeData.ExceptionHandlers.Add( translatorHandlerData); } } 8. Implement the fluent interface members Next, implement each of the members of the fluent interfaces, setting the appropriate fields in translatorHandlerData. After setting the value, each method should return this. Most of these methods can be explicit interface implementations. The resulting code should look like this: public ISqlTranslationRange ToType(Type translatedType) { translatorHandlerData.WrapExceptionType = translatedType; return this; } ISqlTranslationWithMessageOrShielding ISqlTranslationRange.IfInRange( NumericRange range) { translatorHandlerData.ErrorRange = range; return this; }

ISqlTranslationWithShielding ISqlTranslationWithMessage.WithMessage( string message) { translatorHandlerData.ExceptionMessage = message; return this; }


IExceptionConfigurationForExceptionTypeOrPostHandling ISqlTranslationWithShielding.AndShieldOriginalException() { translatorHandlerData.ShieldSqlException = true; return this; }

public IExceptionConfigurationForExceptionTypeOrPostHandling ShieldOriginalException(bool shouldShield) { translatorHandlerData.ShieldSqlException = shouldShield; return this; }


9. Hook your implementation into the fluent configuration builder

EntLibExtensibility HOLs Page 22

The final step is to make your fluent builder available through the fluent configuration API. This is done by writing an extension method over one of the existing methods in the interface. In this case, that interface is IExceptionConfigurationAddExceptionHandlers. Create a new static class named SqlExceptionTranslatorConfigurationSourceBuilderExtensions. Create an extension method in that class named TranslateSqlExceptions. It should extend the IExceptionConfigurationAddExceptionHandlers type. The extension method should return a new instance of the SqlTranslationConfigurationSourceBuilder. The resulting code should look like this: public static class SqlExceptionTranslatorConfigurationSourceBuilderExtensions { public static ISqlTranslationToType TranslateSqlExceptions( this IExceptionConfigurationAddExceptionHandlers context) { return new SqlTranslationConfigurationSourceBuilder(context); } } 10. Test the results The test fixture should now compile and run. Experiment with the test, and see how IntelliSense now guides you through the programmatic configuration experience.

EntLibExtensibility HOLs Page 23

Lab 2: Custom Logging Trace Listener

In this series of exercises, we'll be working through an example of building a custom TraceListener to plug into the Logging Application Block. The handler we'll be building will be addressing a common scenario when debugging applications: providing colored console logging to the application when needed. When logging is set up for applications, it is often configured to log critical messages at run time. However, if that application is interactive or needs to be debugged from a console, it is desirable to then log all or some of these messages to the console to track interaction. Additionally, if a lot of data is logged to the console, it can be difficult to identify critical messages such as warnings or errors from general debug messages. Out of the box, the Logging block does not provide a TraceListener to log output to the console. This is not a difficult listener to create, but it is far more effective when logging messages are colored to make identification of certain items, such as errors, easier. The handler we'll be building will demonstrate this sort of logic it will log trace messages to the console using user-defined colors for various trace message severity types. This hands-on lab includes the following exercises: Exercise 1: Get the handler working with the bare minimum required to get it loading in the configuration tool. Exercise 2: Provide a custom configuration experience in the configuration tool. Exercise 3: Polish the configuration experience to take advantage of things like type pickers, help popups, etc. Exercise 4: Create a fluent interface for the trace listener. Each lab exercise has a "Begin" and "End" directory. For each exercise, open the solution under "Begin" and follow the instructions. If you get stuck, you can look at the code in the End directory to see a working solution. When completing an exercise, do not continue working on the solution you have open. Instead, close your current solution and open the solution in the "Begin" folder for the next exercise. Each starter project in the exercises includes additional code (often tests) that are not in the "End" folder from the previous exercise. This new code is necessary to complete the exercises. This lab assumes familiarity with Enterprise Library, the Exception Handling Block, and C# (including features in C# 3.0).

EntLibExtensibility HOLs Page 24

Exercise 1 - Building the Trace Listener


Introduction
In this exercise, we'll be building out the trace listener, and getting it to work with Enterprise Library configuration.

Estimated Time to Complete: 30 Minutes

Handler Specification
The trace listener were constructing will have the following inputs: consoleColors An element collection of that maps various trace severity types to a console color. windowWidth The width of the created console window if the application is not started from the console. windowHeight The height of the created console window if the application is not started from the console. formatter The trace formatter to use when writing log messages. The base class provides the property, we need to use it however.

Exercise Instructions
1. Open the solution file Open the solution file at <lab root>\ConsoleLoggerBlock\Ex01\Begin\ConsoleLoggerBlock.sln in Visual Studio 2010. Note that the solution includes a skeleton of the trace listener, some utility code, and unit tests. The solution does not currently compile. Upon completion of the exercise, the included tests will pass. 2. Add base class, fields and constructor
Open the file "ConsoleTraceListener.cs". Inside, you'll see the start of the trace listener code, plus a series of // TODO: comments (this will appear in the VS task list).

First, to be used as a trace listener, the class must inherit the CustomTraceListener class from the Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners namespace. Add this base class to the class definition. Next, add a private static System.ConsoleColor field named _defaultColor. This will ensure the console is defaulted correctly after a colored trace line is written. Next, add a private static boolean field named _initialized. Since the console is a static class we dont want multiple listeners re-configuring the console settings so the field must be static. Next, add a private static dictionary named _consoleColorsMap of key type System.Diagnostics.TraceEventType and value type System.ConsoleColor. This will map trace event types to console colors. The following code snippet defaults the dictionary to colors used by many major applications:
private static readonly Dictionary<TraceEventType, ConsoleColor> _consoleColorsMap = new Dictionary<TraceEventType, ConsoleColor> { {TraceEventType.Error, ConsoleColor.Red},

EntLibExtensibility HOLs Page 25

{TraceEventType.Error, ConsoleColor.Red}, {TraceEventType.Warning, ConsoleColor.Yellow}, {TraceEventType.Information, ConsoleColor.Cyan}, {TraceEventType.Start, ConsoleColor.Green}, {TraceEventType.Stop, ConsoleColor.Green}
};

Next, create fields for the inputs listed above, except for the formatter property. Use the ConsoleColorsCollection for the consoleColors property. Finally, create a constructor with parameters assigning values to these fields. Include a parameter of ILogFormatter from the namespace Microsoft.Practices.EnterpriseLibrary.Logging.Formatters and assign it to the base class property of Formatter. For now this should be a private constructor as we dont have a configuration class to use it yet.
The resulting code should look like:
public class ConsoleTraceListener : CustomTraceListener { private static ConsoleColor _defaultColor; private static bool _initialized; private static readonly Dictionary<TraceEventType, ConsoleColor> _consoleColorsMap = new Dictionary<TraceEventType, ConsoleColor> { {TraceEventType.Error, ConsoleColor.Red}, {TraceEventType.Warning, ConsoleColor.Yellow}, {TraceEventType.Information, ConsoleColor.Cyan}, {TraceEventType.Start, ConsoleColor.Green}, {TraceEventType.Stop, ConsoleColor.Green} }; private readonly ConsoleColorsCollection _consoleColors; private readonly int _windowHeight; private readonly int _windowWidth;

private ConsoleTraceListener(ConsoleColorsCollection consoleColors, ILogFormatter formatter, int windowHeight, int windowWidth) { _consoleColors = consoleColors; Formatter = formatter; _windowHeight = windowHeight; _windowWidth = windowWidth; } }

3. Add the initialization code Initializing the console block requires that you write some code to ensure that the console can be set up correctly and then configure the newly created settings. This code should be in the constructor and be created as two methods. First an Initialize() method that returns a Boolean indicating if configuration settings should be set. The result of this should be conditionally checked and if successful should call a method named SetupConfigurationData(). The resulting constructor should look like:
public ConsoleTraceListener(ConsoleColorsCollection consoleColors, ILogFormatter formatter,

EntLibExtensibility HOLs Page 26

ILogFormatter formatter, int windowHeight, int windowWidth) {


_consoleColors = consoleColors; Formatter = formatter; _windowHeight = windowHeight; _windowWidth = windowWidth; if (Initialize()) { SetupConfigurationData(); } }

Next, create a private Initialize() method that initialize the console. The code in this method should do the following:

a. Check to see whether the _initialized roperty is set and return false if it is. If it is not set, set the field to true so that this method will not be called a second time. b. Assign the consoles Standard Out to the output stream and the Standard Error Stream to the error stream. This is mainly needed since some application hosts may override this and no output would be logged. The AutoFlush property should be set as well. This code is tricky, so it should look like this:
Console.SetOut(new System.IO.StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); Console.SetError(new System.IO.StreamWriter(Console.OpenStandardError()) { AutoFlush = true });

c. Set the value of _defaultColor to Console.ForegroundColor. This will ensure the console is defaulted correctly after a colored trace line is written. d. Return true to indicate that configuration settings should be set.

The resulting code should look like:


private bool Initialize() { if (_initialized) return false; _initialized = true;
Console.SetOut(new System.IO.StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); Console.SetError(new System.IO.StreamWriter(Console.OpenStandardError()) { AutoFlush = true });

_defaultColor = Console.ForegroundColor;
return true;

Finally, create a private SetupConfigurationData() method to initialize the configuration settings. The method should perform the following actions:
a. If the _windowHeight setting is greater than 0, set the value to Console.WindowHeight. b. If the _windowWidth setting is greater than 0, set the value to Console.WindowWidth and Console.BufferWidth. c. Finally, if the _consoleColors field is not null, assign its various trace type keys and console color
EntLibExtensibility HOLs Page 27

c. Finally, if the _consoleColors field is not null, assign its various trace type keys and console color values to the _consoleColorsMap dictionary. The parsing logic here is somewhat complex, so there is a helper class ConsoleColorParser in the project with a method called Parse() that takes the color collection in along with the map and assigns and parsed colors. The resulting method should look like:
private void SetupConfigurationData() { if (_windowHeight > 0) Console.WindowHeight = _windowHeight; if (_windowWidth > 0) { Console.WindowWidth = _windowWidth; Console.BufferWidth = _windowWidth; }
if (_consoleColors != null) ConsoleColorParser.Parse(_consoleColors, _consoleColorsMap);

4. Override the CustomTraceListener methods to log to the console. We need to override several base methods from the CustomTraceListener class to log data to the console. These methods are:
void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data) void Write(string message)

void WriteLine(string message)

First, the Write() and WriteLine() methods should simply implement Console.WriteLine() and Console.Write() respectively. More assumptions can be made about logging here as a personal exercise, but these methods are rarely called by the logging block as opposed to the TraceData() method.

Next implement the TraceData() method. The method will need to do the following: a. Check to see if the Filter property is not null. This property is the trace filter that is created which we want to check before writing the message. If the property is not null, call Filter.ShouldTrace() filling in the parameters from the TraceData() method. The parameters formatOrArgument, args and data should be set to null. b. Check the _consoleColorsMap to see if it contains an entry for the eventType parameter. If it does, set the Console.ForegroundColor to that entry, otherwise set it to the _defaultColor property. c. Next, check to see if the data parameter is of a LogEntry type of the namespace Microsoft.Practices.EnterpriseLibrary.Logging. d. If the entry is of type LogEntry then check to see if the Formatter property is not null. Create a variable named message and assign the output of Formatter.Format(data as LogEntry) to it. If it is null, call data.ToString() to get the text output of the object. There is no null check here as at this point the data can never be null. Then call the WriteLine() method to output the data. e. If the entry is not of type LogEntry call the base method to pass it along to the base handler. f. Finally, set the Console.ForgroundColor back to to the _defaultColor value. The resulting methods should look like:
public override void Write(string message) {

EntLibExtensibility HOLs Page 28

{
Console.Write(message); }

public override void WriteLine(string message) { Console.WriteLine(message); }


public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data) { if ((Filter != null) && !Filter.ShouldTrace(eventCache, source, eventType, id, null, null, data, null)) return; Console.ForegroundColor = _consoleColorsMap.ContainsKey(eventType) ? _consoleColorsMap[eventType] : _defaultColor; if (data is LogEntry) { var message = Formatter != null ? Formatter.Format(data as LogEntry) : data.ToString(); WriteLine(message); } else { base.TraceData(eventCache, source, eventType, id, data); } Console.ForegroundColor = _defaultColor;

5. Hook up to Entlib Configuration We will be using the bare bones configuration support in Enterprise Library for this exercise. Each Entlib block allows for a "custom" handler or provider; this allows you to specify an arbitrary set of name/value pairs in configuration. You get no special design time support, but it is less work to implement the handler. To do this, there are two things required.

First, add the ConfigurationElementType attribute to your handler class. This attribute takes the type of the configuration element. For a custom handler, that type is CustomerTraceListenerData. The result should look like this:
[ConfigurationElementType(typeof(CustomTraceListenerData))]

Second, a new public constructor must be added to the handler. This must not take any arguments as this basic data configurator can only set the log formatter property via its property setter. For this reason we will pass null and zero values into the constructor we already created to default all the values. This will not allow for advanced configuration at this point, but that will be completed in the next exercise. The resulting constructor should look like this:
public ConsoleTraceListener() : this(null, null, 0, 0) { }

EntLibExtensibility HOLs Page 29

6. Test the result


Set the MyLoggingApplication to be the startup application. Then run the project to see it logging messages out to the console, using the default colors for the various message severity types.

If you need help, or would like to see a working version, look at the solution in the End directory of the exercise.

EntLibExtensibility HOLs Page 30

Exercise 2 - Creating a Custom Design Experience


Introduction
In this exercise, we'll be extending our initial trace listener to provide for a better design experience. Estimated Time to Complete: 15 Minutes

Exercise Instructions
1. Open the solution file. Open the solution file at <lab root>\ConsoleLoggerBlock\Ex02\Begin\ConsoleLoggerBlock.sln in Visual Studio 2010. Note that the solution includes the trace listener from before and a new empty ConsoleTraceListenerData class, plus a series of // TODO: comments (this will appear in the VS task list). 2. Update the configuration attribute. Open the ConsoleTraceListener.cs file and change the implementing type of the ConfigurationElementType attribute from CustomTraceListnerData to ConsoleTraceListenerData.
3. Update the ConsoleTraceListener constructors. Remove the empty constructor from the class as it will no longer be needed. Then change the constructor with the parameters to be public so the configuration handler can create a new instance of the class.

4. Implement the base class Open the ConsoleTraceListenerData file. This class must inherit from the TraceListenerData class in the Microsoft.Practices.EnterpriseLibrary.Logging.Configuration namespace. 5. Create the constants and properties The system configuration setup needs to know about configuration items in several spots for the properties we are going to create. For this reason, its easiest to create constants in the class to use these properties. Create one for formatter, consoleColors, windowHeight, and windowWidth.
Next, create a property for each of the constants above. This should be ConsoleColors, Formatter (use type string for this property), WindowHeight and WindowWidth. Since ConsoleColors is a collection, it only needs a getter.

These properties need to be implemented in the same way a configuration property would be implemented: namely with a ConfigurationProperty attribute from the namespace System.Configuration that takes a single argument of one of the previously created constants. Each get method should return (<property type>) base*<property constant>+ and each set method should follow the pattern base*<property constant>+=value; One of the properties, formatter, needs an additional attribute so the designer knows to link this property to the formatter collection. For this reason you must add a Reference attribute from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Design namespace. This attribute is provided in the same as a comment since the syntax is somewhat complex. When this is complete you should have properties that look like:
EntLibExtensibility HOLs Page 31

When this is complete you should have properties that look like:
private const string CONSOLE_COLORS = "consoleColors"; private const string FORMATTER = "formatter"; private const string WINDOW_HEIGHT = "windowHeight"; private const string WINDOW_WIDTH = "windowWidth";

[ConfigurationProperty(CONSOLE_COLORS)] public ConsoleColorsCollection ConsoleColors { get { return (ConsoleColorsCollection)base[CONSOLE_COLORS]; } }

[ConfigurationProperty(FORMATTER)] [Reference( typeof(NameTypeConfigurationElementCollection<FormatterData, CustomFormatterData>), typeof(FormatterData))] public string Formatter { get { return (string)base[FORMATTER]; } set { base[FORMATTER] = value; } }
[ConfigurationProperty(WINDOW_HEIGHT)] public int WindowHeight { get { return (int)base[WINDOW_HEIGHT]; } set { base[WINDOW_HEIGHT] = value; } } [ConfigurationProperty(WINDOW_WIDTH)] public int WindowWidth { get { return (int)base[WINDOW_WIDTH]; } set { base[WINDOW_WIDTH] = value; } }

6. Override the GetCreationExpression() method

The base class has a method that needs to be overridden to set the various property values in the constructor. This method returns an lambda expression that is used to create a new instance of the ConsoleTraceListener class. When you override this method, create an return statement with an empty lambda return () => that creates a new instance of the ConsoleTraceListener class, using the getter of each of the properties created to assign the values of the constructor. The special case here is the Formatter property. Notice that it expects a ILogFormatter class but the property type is a string. To turn the string reference into a formatter, simply use the Container helper class from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ContainerModel namespace with the ResolvedIfNotNull method. The method is generic and requires you specify the type you wish to implement. It also takes in a string parameter (the formatter name) and will attempt to resolve that named item to the type supplied in the generic type. For this exercise, that method should be the generic type of ILogFormatter and use the Formatter property as the parameter.
The resulting method should look like:
protected override Expression<Func<TraceListener>> GetCreationExpression() {

EntLibExtensibility HOLs Page 32

{ return () => new ConsoleTraceListener(ConsoleColors, Container.ResolvedIfNotNull<ILogFormatter>(Formatter), WindowHeight, WindowWidth); }

7. Update the application configuration file Open the App.config file in the MyLoggingApplication project. Locate the element loggingConfiguration -> listeners ->add. That element contains a listenerDataType attribute that needs to be set to our new ConsoleTraceListenerData class. The final attribute string should look like:
listenerDataType="Microsoft.Entlib.Logging.ConsoleTraceListener.ConsoleTraceListenerD ata, ConsoleLogger, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

You will notice that there are windowHeight and windowWidth attributes already supplied as part of the configuration, these are to demonstrate that we are not picking up the new attributes.
8. Test the results Set the MyLoggingApplication to be the startup application. Then run the project to see it logging messages out to the console, using the default colors for the various message severity types. You will also notice that the console window resizes to match the values provided.

If you need help, or would like to see a working version, look at the solution in the End directory of the exercise.

NOTE: You may see the console log an error about the console width or height value being out of range. This is due to the fact that console size is restricted by the operating system defaults. To correct this, change the value to a value below the value suggested in the error. To observe the results of the block on the configuration tool, copy the ConsoleLogger.dll assembly into the Reference Assemblies\EntLib 5 directory so the configuration too recognizes the block. When you run the configuration tool you should be able to add a Logging Block section and a Console Trace Listener in the Trace Listener section.

EntLibExtensibility HOLs Page 33

Exercise 3 - Enhancing the Custom Design Experience


Introduction
In this exercise, we'll be extending our initial design experience to provide defaults to our configuration properties as well as names and values for the settings and provider name. We will also allow the console colors section to be configured via the designer.
Estimated Time to Complete: 15 Minutes

Exercise Instructions
1. Open the solution file. Open the solution file at <lab root>\ConsoleLoggerBlock\Ex03\Begin\ConsoleLoggerBlock.sln in Visual Studio 2010. Note that the solution includes the trace listener and data section from before, plus a series of // TODO: comments (this will appear in the VS task list). 2. Create a resource file In the solution, right click on the ConsoleTraceListener project, go to Add -> New Item. In the dialog locate the Resource file type (.resx) and create a new item named Resources.resx. Then drag the newly created item into the Properties folder. This will allow for properties to be internationalized. Next, open the file by double clicking and create display names and descriptions for your listener properties. A recommended format for this would be ConsoleTraceListenerData<property name>DisplayName for the display name property and ConsoleTraceListenerData<property name>Description for the tool tip description. Also create the name items (omitting the property name portion) to show a friendly name and description for the listener itself when you add it. If you need help with this portion look at the solution in the End directory for assistance. 3. Add display and description attributes. The design utility needs attributes applied to the properties and class definition to inform it that resources exist for the display name and description. To provide the display name youll need to add a ResourceDisplayName attribute of the Microsoft.Practices.EnterpriseLibrary.Common. Configuration.Design namespace. This attribute takes the type of your resource class as the first parameter, and a string of the resource name for what should be displayed. Similarly you add the attribute ResourceDescription for the description. So, for example the resource display name for the class would look like:
[ResourceDisplayName(typeof(Resources), "ConsoleTraceListenerDataDisplayName")] [ResourceDescription(typeof(Resources), "ConsoleTraceListenerDataDescription")]

Do this for the class and each of the properties in the ConsoleTraceListenerData class. 4. Add default values for some properties

In some cases, a developer may not want the WindowHeight and WindowWidth properties to be set to 0, but a standard console size for debugging. This can be easily achieved by setting the DefaultValue property of the ConfigurationProperty attribute for these classes. For the exercise we will default WindowHeight to 70 and WindowWidth to 100. The a completed property should look
EntLibExtensibility HOLs Page 34

will default WindowHeight to 70 and WindowWidth to 100. The a completed property should look like:
[ConfigurationProperty(WINDOW_WIDTH, DefaultValue = 100)]

5. Add console colors editor The design time experience would not be complete without the addition of a collection editor to add console color overrides. To simplify the exercise, the WPF editor code and view models for the configuration tool have been created and are located within the Design directory of the project. The first step to adding the editor is to indicate that the ConsoleColors property in the ConsoleColorTraceListenerData class is not read-only at design time. This can be done by adding the DesignTimeReadOnly attribute from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Design namespace with a value of false. Next, we need to indicate what editor we want to use, which in this case is the CollectionEditor. This is done be adding the Editor attribute from the System.ComponentModel namepace with the parameters of CommonDesignTime.EditorTypes.CollectionEditor and CommonDesignTime.EditorTypes.FrameworkElement to indicate we want a collections editor and it is based off a WPF framework element. Once complete, the ConsoleColors property should look like:
[ConfigurationProperty(CONSOLE_COLORS), ResourceDisplayName(typeof(Resources), "ConsoleTraceListenerDataConsoleColorsDisplayName"), ResourceDescription(typeof(Resources), "ConsoleTraceListenerDataConsoleColorsDescription"), DesignTimeReadOnly(false), Editor(CommonDesignTime.EditorTypes.CollectionEditor, CommonDesignTime.EditorTypes.FrameworkElement)] public ConsoleColorsCollection ConsoleColors { get { return (ConsoleColorsCollection)base[CONSOLE_COLORS]; } }

Finally, we need to add some additional attributes to the ConsoleColorElement class to have it show up with a color editor and provide drop downs for the trace type. To do this: a. add a ViewModel attribute from the namespace Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Design to the class with a type parameter of ConsoleColorCollectionElementViewModel. This is a custom view model in the project that is used to create a hierarchical layout for the elements. b. Then, add another ViewModel attribute to the Name property of the type TraceNameViewModel. This is another custom view model used to provide suggested trace name values and avoid duplicate trace values in the configuration. c. Last, add an Editor attribute to the Color property with the first type of ColorPickerEditor and the second type of FrameworkElement. This will wire up a custom color picker to visually select the color desired for the trace type.

If you need help, refer to the End solution in this exercise.


6. Test the results Compile the project, copy the ConsoleLogger.DLL file to the <lab root>\Reference Assemblies\Entlib 5 directory, and then open the configuration file in the Entlib config tool. Navigate to the logger section
EntLibExtensibility HOLs Page 35

directory, and then open the configuration file in the Entlib config tool. Navigate to the logger section and click the add trace listener button. Youll notice the name and description you provided are there. Also notice that the properties of the handler have friendly names and descriptions along with default values for Window Height and Window Width and a friendly color picker.

EntLibExtensibility HOLs Page 36

Exercise 4 - Creating a Fluent Interface for the Trace Listener

Introduction
Enterprise Library 5 introduced a new way of performing programmatic configurationthe fluent configuration builders. This approach lets you write code to configure Entlib that is fairly straightforward, easy to read later, and leverages IntelliSense in Visual Studio to guide you in writing your configuration code.

The fluent configuration interface is extensible. In this exercise, we'll be adding fluent configuration support for our new handler. Estimated Time to Complete: 30 minutes

Designing the Fluent Interface


The first step is to design the grammar of the language you want to implement in your fluent interface. The exception block's fluent configuration starts like this:
var builder = new ConfigurationSourceBuilder(); builder.ConfigureLogging() .WithOptions .SpecialSources.AllEventsCategory .WithOptions .SendTo...

At this point we want to plug in the trace listener configuration. The full grammar we want to implement is:
var builder = new ConfigurationSourceBuilder(); builder.ConfigureLogging() .WithOptions .SpecialSources.AllEventsCategory .WithOptions .SendTo.Console("ConsoleTraceListener") .FormatWith(new FormatterBuilder() .TextFormatterNamed("Text Formatter") .UsingTemplate("{timestamp} {severity} {message}")) .WithConsoleColors() .SetConsoleColor(TraceEventType.Critical, ConsoleColor.Red) .SetConsoleColor(TraceEventType.Error, ConsoleColor.Red) .SetConsoleColor(TraceEventType.Warning, ConsoleColor.Cyan) .Complete() .WithWindowHeight(50) .WithWindowWidth(150);

(This example is included in the Program.cs file in the test project.)


When building these fluent interfaces, the approach taken in Entlib is to represent each step of the "sentence" as a separate interface. This allows us to force users to specify things in a specific order, guarantee they only happen once, and control which statements are optional and which are required. It also allows for easy composition and combination later.

In our grammar there is no required order; however, the WithConsoleColors section is setting up a collection and we want the developer to notice this; therefore, we enter a new depth of configuration that forces the developer to call a Complete method to exit the collection configuration and return to configuring the trace listener.
EntLibExtensibility HOLs Page 37

configuring the trace listener.

Exercise Instructions
1) Open the solution file.
Open the solution file at <lab root>\ConsoleLoggerBlock\Ex04\Begin\ConsoleLoggerBlock.sln in Visual Studio 2010. Note that the solution includes the trace listener and data section from earlier, plus a series of // TODO: comments (this will appear in the Visual Studio task list).

2) Create the static method for the Console step. Create the Console(<logger name>) method. This is an extension method that will return our trace listener configuration interface ILoggingConfigurationSendToConsoleTraceListener. To create this, add a public static class named LoggingConsoleListenerConfigurationExtensions in the Config folder. This method will also need to return an implementation of the interface, so create a class named LoggingConfigurationSendToConsoleTraceListener that will implement the ILoggingConfigurationSendToConsoleTraceListener interface and an additional one that well create in the next step. Logging extension methods that add a trace listener use the ILoggingConfigurationSendTo interface from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Fluent namespace as their reference (this) argument. A string argument, listener name, is also required. The completed class should look like this:
public static class LoggingConsoleListenerConfigurationExtensions { public static ILoggingConfigurationSendToConsoleTraceListener Console( this ILoggingConfigurationSendTo context, string listenerName) { return new LoggingConfigurationSendToConsoleTraceListener( context, listenerName); } }

3) Create the trace listener command interface. The ILoggingConfigurationSendToConsoleTraceListener interface needs methods for setting the WindowHeight, WindowWidth and ConsoleColors properties as well as for setting the basic trace options, formatter and filter. The basic intent of a fluent interface is that it allows you to keep configuring it so each configuration method should return the configuration interface, with the exception of ConsoleColors, which will return a new configuration interface. Begin by adding a new interface ILoggingConfigurationSendToConsoleTraceListener to the Config folder. This interface should extend the the ILoggingConfigurationCategoryContd interface from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Fluent namespace. This will allow you to add other trace listeners or another trace category later. Next, add method signatures to configure the common listener properties. Other listeners have already defined what these methods should look like. These include Filter(SourceLevels) for source level filtering, FormatWith(IFormatterBuilder) to add a trace formatter, FormatWithSharedFormatter(string) to add a reference to a shared trace formatter, and WithTraceOptions(TraceOptions) to configure trace options. Each of these methods should return the interface to allow continued configuration. Then, add the methods that are specific to our grammar. The general grammar is to start the method using the phrase With and the property name. In our case we need to create methods for WithWindowHeight(int) and WithWindowWidth(int). We also need a method to configure console colors WithConsoleColors(). Notice this method does not accept any argument as its going to return
EntLibExtensibility HOLs Page 38

colors WithConsoleColors(). Notice this method does not accept any argument as its going to return an interface that sets up the console colors collection. This interface will be "ILoggingConfigurationSendToConsoleTraceListenerConsoleColors" and well be creating it in the next step. Once completed, the interface should look like this:
public interface ILoggingConfigurationSendToConsoleTraceListener : ILoggingConfigurationCategoryContd { ILoggingConfigurationSendToConsoleTraceListener Filter(SourceLevels sourceLevel);
ILoggingConfigurationSendToConsoleTraceListener FormatWith( IFormatterBuilder formatBuilder); ILoggingConfigurationSendToConsoleTraceListener FormatWithSharedFormatter( string formatterName);

ILoggingConfigurationSendToConsoleTraceListenerConsoleColors WithConsoleColors();
ILoggingConfigurationSendToConsoleTraceListener WithTraceOptions( TraceOptions traceOptions);

ILoggingConfigurationSendToConsoleTraceListener WithWindowHeight(int windowHeight); ILoggingConfigurationSendToConsoleTraceListener WithWindowWidth(int windowWidth); }

4) Create the ConsoleColor collection steps.

As mentioned in the previous step, were creating an "ILoggingConfigurationSendToConsoleTraceListenerConsoleColors" interface to configure the console colors collection. This interface will have two methods. The first is SetConsoleColor(TraceEventType, ConsoleColor), which will set a color for an event type. This method will return the console colors interface to allow additional colors to be configured. The second is Complete(), which will return us to our parent configuration interface. Once complete, this interface should look like this:
public interface ILoggingConfigurationSendToConsoleTraceListenerConsoleColors { ILoggingConfigurationSendToConsoleTraceListener Complete(); ILoggingConfigurationSendToConsoleTraceListenerConsoleColors SetConsoleColor(TraceEventType eventType, ConsoleColor consoleColor);
}

5) Implement the step interfaces. In step 2, we created a static method that returns to us a configuration class named LoggingConfigurationSendToConsoleTraceListener. This class will serve as the implementation for our interfaces and configure the underlying element. The first step is to have the class implement the SendToTraceListenerExtension class from the Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Fluent interface. Next have it implement both the ILoggingConfigurationSendToConsoleTraceListener and ILoggingConfigurationSendToConsoleTraceListenerConsoleColors interfaces. This may seem counterintuitive, but the pattern will soon become obvious. Next, add a private field for the ConsoleTraceListenerData configuration element on which were going to set configuration properties. Then, implement the required constructor that accepts the ILoggingConfigurationSendTo context argument and a string for the listener name. Pass the context into the base class constructor and assign the private field a new ConsoleTraceListenerData element with
EntLibExtensibility HOLs Page 39

the base class constructor and assign the private field a new ConsoleTraceListenerData element with the listener name and default trace options passed in. Also make sure the base method AddTraceListenerToSettingsAndCategory(_consoleTraceListener) is called so the configuration block knows about the listener configuration we just created. It is critical that this method is called. Once complete, the initial class should look like this:
internal class LoggingConfigurationSendToConsoleTraceListener : SendToTraceListenerExtension, ILoggingConfigurationSendToConsoleTraceListenerConsoleColors, ILoggingConfigurationSendToConsoleTraceListener { private readonly ConsoleTraceListenerData _consoleTraceListener;
public LoggingConfigurationSendToConsoleTraceListener( ILoggingConfigurationSendTo context, string listenerName) : base(context) { _consoleTraceListener = new ConsoleTraceListenerData( listenerName, TraceOptions.None); AddTraceListenerToSettingsAndCategory(_consoleTraceListener); } }

Then, implement the various methods, assigning the arguments to the appropriate properties in the _consoleTraceListener field. After each assignment, add return this to return the configuration element for the next configuration. One exception to this is the FormatWith method. This requires some additional configuration that gets the formatter data and adds it to the master collection and returns the name. For this weve created a helper method called CreateFormatter(IFormatBuilder) that returns the formatter name. Assign this name to the Formatter property as you would with the FormatWithSharedFormatter method. You will notice that the ILoggingConfigurationSendToConsoleTraceListenerConsoleColors interface is also implemented even though it is a separate configuration element. This is mainly done to force the user into a grammar structure that configures colors separately. The SetConsoleColor method should add a new ConsoleColorElement to the trace listener ConsoleColors collection property and the Complete method should simply return this. If you get stuck, reference the End solution to see how individual methods are completed. 6) Test the results. Once this is complete, compile the solution (the reference application should compile to the grammar we created) and run the solution. Youll notice the logging block works as before when it was configured in the app.config file, but there is no configuration currently in the file.

EntLibExtensibility HOLs Page 40

Lab 3: Custom Configuration Source

Configuration is an essential part of Enterprise Library. Typically, configuration information is stored in the .NET standard application configuration file either app.config or web.config. However, configuration is abstracted behind the idea of a Configuration Source. This allows you to provide your own sources to read configuration from other locations. In this series of exercises, you will be building a configuration source that hooks to the Windows Registry. You'll see what's required to provide configuration information, write it back, monitor for changes, and make your config source available in the config tool.

This lab contains four exercises:


Exercise 1: Create the configuration source and use it to load configuration information from a pre-populated registry key. Exercise 2: Add the ability to write configuration information to the registry. Exercise 3: Add change tracking notifications - when the registry changes, the configuration source will signal the rest of Enterprise Library. Exercise 4: Add configuration information and make your new configuration source available in the Entlib configuration tool. Each lab exercise has a "Begin" and "End" directory. For each exercise, open the solution under "Begin" and follow the instructions. If you get stuck, you can look at the code in the End directory to see a working solution. The lab solutions include a set of helper classes for dealing with the registry, so you will not have to implement them directly. They will be explained as needed in the instructions when you reach steps in the exercises that need them. When completing an exercise, do not continue working on the solution you have open. Instead, close your current solution and open the solution in the "Begin" folder for the next exercise. Each starter project in the exercises includes additional code (often tests) that are not in the "End" folder from the previous exercise. This new code is necessary to complete the exercises. This lab assumes familiarity with Enterprise Library and C# (including features in C# 3.0).

EntLibExtensibility HOLs Page 41

Exercise 1 - Reading From the Registry


Introduction
In this exercise, you will create a registry configuration source and provide enough implementation for it to retrieve configuration sections from the registry. Estimated Time To Complete: 15 minutes

Exercise Instructions
1. Open the Solution file

Open the solution file at <lab root>\RegistryConfigurationSource\Ex01\Begin\RegistryConfigurationSource.sln in Visual Studio 2010. Note that the solution already contains some helper code and tests. The solution will not compile at this point; you need to finish the exercise and add your code first. The "Registry" folder contains a set of helper classes and isolation interfaces to manage working with the Windows registry. The raw Registry API has been isolated behind these interfaces to make testability easier. Since the details of registry interaction are not important to the lab exercise, you will simply be using these helpers as part of building your configuration source.
2. Add Entlib core reference to the RegistryConfiguration project Configuration sources are objects that implement the IConfigurationSource interface. This interface is defined in the Microsoft.Practices.EnterpriseLibrary.Common.dll. Add a reference to this DLL to the RegistryConfiguration project. 3. Add RegistryConfigurationSource class to your project Add a new class to the RegistryConfiguration project. It must be a public class and implement the IConfigurationSource interface. To begin with, implement all the methods of the interface so that they threw a NotImplementedException, except for Dispose() which should be empty (we have nothing to dispose right now, we'll be using it later though). The resulting code should look like this: using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; using System; using System.Configuration;

namespace RegistryConfiguration { public class RegistryConfigurationSource : IConfigurationSource { public void Dispose() { } public ConfigurationSection GetSection(string sectionName) { throw new NotImplementedException(); }
public void Add(string sectionName, ConfigurationSection configurationSection) { throw new NotImplementedException(); } public void Remove(string sectionName) { throw new NotImplementedException();

EntLibExtensibility HOLs Page 42

throw new NotImplementedException(); } public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) { throw new NotImplementedException(); } public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) { throw new NotImplementedException(); } public event EventHandler<ConfigurationSourceChangedEventArgs> SourceChanged; } }

4. Add a constructor and helper objects as parameters


The configuration source needs to know where in the registry to pull its data from. Since we're accessing the registry through helper objects rather than going directly (to allow for testing), we also need to provide a helper object that gets registry keys. Add a constructor that takes two parameters. The first is of type IRegistryKeyFactory (defined in the Registry folder) named keyFactory, and the second is a string named rootPath, which will be the registry path to read from. Store these objects into fields. Don't forget to add a using reference to RegistryConfiguration.Registry. The resulting code should look like: private readonly IRegistryKeyFactory keyFactory; private readonly string rootPath; public RegistryConfigurationSource(IRegistryKeyFactory keyFactory, string rootPath) { this.keyFactory = keyFactory; this.rootPath = rootPath; } 5. Introducing the GetSection method The GetSection() method is the workhorse of a configuration section. It is responsible for going out, finding the configuration information, and populating and returning the correct configuration section object. For the registry source, it will look for a key under the provided root with the name name as the section requested. The default (unnamed) value under that key will be the full type name for the configuration section object itself. Actually loading the section requires use of an XmlReader. The following steps will cover how to implement each of these pieces. Start by deleting the "throw new NotImplementedException();" line from the GetSection() method. 6. Open the root registry key First, we need a way to read the registry. The keyFactory that was passed into the constructor can be used to open up the required registry key. Inside a using statement, call the keyFactory.GetReadKey() method, passing the rootPath field. This gives an object (IReadableRegistryKey) that provides access to the contents of that registry key. Store the result of GetReadKey() in a variable named rootKey.

7. Find the key matching the section Loop through each object in the rootKey.Subkeys collection. Check the Name property of the subkey and see if it matches the sectionName passed into GetSection(). If no subkeys match, return null.

EntLibExtensibility HOLs Page 43

(Note: the individual subkeys do not be disposed, they'll be disposed when their parent key is.) 8. Get the section type and create an instance of the section Using the subkey's DefaultValue property, call Type.GetType() to get the type object for the configuration section. Then call Activator.CreateInstance() to create an instance of the configuration section itself. Cast the instance to SerializableConfigurationSection.

9. Load the data from the registry into the configuration section Entlib configuration sections are responsible for loading themselves. You need to supply an implementation of XmlReader and pass it to the section's ReadXml() method.
Create an instance of RegistryXmlReader, passing it the subkey containing the configuration information for the section. Then pass that XML reader to the section's ReadXml() method. Then return the loaded section object. The resulting GetSection() method should look like: public ConfigurationSection GetSection(string sectionName) { using (var rootKey = keyFactory.GetReadKey(rootPath)) { foreach (var subkey in rootKey.Subkeys) { if (subkey.Name == sectionName) { var sectionTypeName = subkey.DefaultValue; var sectionType = Type.GetType(sectionTypeName); var section = (SerializableConfigurationSection) Activator.CreateInstance(sectionType); var reader = new RegistryXmlReader(subkey); section.ReadXml(reader); return section; }

} return null;
} } Note: When implementing a custom XmlReader over a data store, there are a couple of issues to keep in mind to get it working with Entlib config sections. First, you must include an XML declaration; configuration sections don't work on XML fragments, despite the fact that they are stored as separate elements in a single XML file. Second, if you have empty elements in your XML, the XmlReader must represent them as a single Element node with the IsEmptyElement flag, as opposed to representing the node as an Element/EndElement node pair. These: <clear /> and <clear></clear> may be semantically identical XML, but the config sections don't treat them that way. 10. Run the tests to verify correctness

Run the test suite. The tests under the "ReadingRegistryAsXml" folder use mock objects to avoid hitting the actual registry, while the tests in the IntegrationTests folder do actually read and write from the registry. If you run into difficulties, you can look at the project in the <lab root>\RegistryConfigurationSource\Ex01\End directory for working code for this exercise.

EntLibExtensibility HOLs Page 44

Exercise 2 - Writing to the Registry

Introduction
In this exercise, you will add the ability to add and remove sections from the configuration source.

Estimated Time to Complete: 10 minutes

Exercise Instructions
1. Open the Solution file
Open the solution file at <lab root>\RegistryConfigurationSource\Ex02\Begin \RegistryConfigurationSource.sln in Visual Studio 2010.

2. Implement the Remove method There are two methods you'll need to implement Add() and Remove(). Remove() is the simpler of the two so you'll start there. Remove the "throw new NotImplementedException();" line from the RegistryConfigurationSource.Remove() method. Then, inside a using statement, use the keyFactory to get the rootKey again, like you did in Exercise 1. However, use the GetWriteKey() method on the keyFactory this time, not the GetReadKey() method. Then use this returned key to delete the subkey with the given section name. This method will simply do nothing if the section isn't in the registry.
The resulting implementation should look like:

public void Remove(string sectionName) { using (var rootKey = keyFactory.GetWriteKey(rootPath)) { rootKey.DeleteSubKey(sectionName); } } 3. Implement the Add method Next, you will add an implementation of the Add() method. Remove the "throw new NotImplementedException();" line from the body of the RegistryConfigurationSection.Add() method. Next, in a using block, get the root key using keyFactory.GetWriteKey(). The logic needed to write out the contents of a section has been encapsulated into the RegistrySectionWriter class. Create an instance of this class, passing the root key you've already opened.

EntLibExtensibility HOLs Page 45

Finally, call the RegistrySectionWriter.WriteSection() method, passing the section object to add and the name to save it under. You will have to cast the section to the SerializableConfigurationSection type in order to do this. The resulting code should look like this: public void Add(string sectionName, ConfigurationSection configurationSection) { using (var rootKey = keyFactory.GetWriteKey(rootPath)) { var sectionWriter = new RegistrySectionWriter(rootKey); sectionWriter.WriteSection( (SerializableConfigurationSection) configurationSection, sectionName); } } Note: most of the RegistrySectionWriter implementation is around the details of writing to the registry. There is one important detail that is useful to know when implementing your own configuration sources: how to get the XML out of the section. SerializableConfigurationSection has a WriteXml() method that takes an XmlWriter parameter. The usual expectation here is that you could provide your own custom implementation of XmlWriter, and as the config system calls the various methods on XmlWriter to create elements, attributes, etc. you could respond to them by writing out to your final store.
Unfortunately, the System.ConfigurationSection class, when WriteXml() is called, doesn't do that. Instead, it calls XmlWriter.WriteRaw(), passing a string which has the entire XML in it, including the XML declaration. In addition, the root node of the XML string you get isn't the name of the section, but instead is always SerializableConfigurationSection.

If all you need to do is store a blob, this is fine. However, for our registry example that isn't sufficient, as we want to store individual XML elements as keys in the registry. So what the RegistySectionWriter actually does is set up an XmlTextWriter, pass that to the section's WriteXml message, and pulls that string back out. Then it wraps that string of XML in an XmlReader and works through the XML using that reader. If you are interested, the details of the implementation are in the RegistrySectionWriter.SectionToXmlReader() method. 4. Test the results Run the unit tests, particularly the WritingToPhysicalRegistryFixture. All tests should pass. After running the unit test, open regedit and look at the HKEY_CURRENT_USER\Software\Microsoft \Practices\EntlibWorkshop\WriteTest registry key. You should see the newly written registry information containing configuration for the logging block.

EntLibExtensibility HOLs Page 46

Note: The digits are prepended to the key names for two reasons. First, XML schemas often require elements to occur in a specific order, whereas the registry does not support this. Keys simply arrive in order sorted by name. The digits force the ordering to be correct. Second, XML lets you have multiple tags with the same name under the same parent (two add elements under listeners, for example). The registry does not. Adding the digits lets us store tags with the same name. The RegistrySectionWriter handles adding the digits to the key names while writing. The RegistryXmlReader removes the digits during the read process. If you run into difficulties, you can look at the project in the <lab root>\RegistryConfigurationSource \Ex02\End directory for working code for this exercise.

EntLibExtensibility HOLs Page 47

Exercise 3 - Monitoring for Changes


Introduction
In this exercise, you will add support for change detection - when the underlying registry key changes, the configuration source will raise an event. This lets the parts of Entlib that are looking for such things (such as the logging block) reconfigure at runtime as configuration changes. NOTE: monitoring the registry for changes requires calling unmanaged code, so this will not work in hosting environments without the call to unmanaged code permissions. Estimated Time to Complete: 15 minutes

Exercise Instructions
1. Open the solution file Open the solution file at <lab root>\RegistryConfigurationSource\Ex03\Begin \RegistryConfigurationSource.sln in Visual Studio 2010. 2. Add a field for the change monitor The details of monitoring the registry for changes has been encapsulated in the IRegistryKeyChangeMonitor class. In the RegistryConfigurationSource class, add a field to the configuration source named changeMonitor to store this monitor object.
private IRegistryKeyChangeMonitor changeMonitor;

3. Add the StartMonitoringRegistry() method Add a new public method named StartMonitoringRegistry(). Check if the changeMonitor field is null. If it is, call the keyFactory.GetChangeMonitor() method to get a change monitor object. Hook up the changeMonitor.Changed event to a method named OnSourceChanged(), which we'll write next.
Also call the changeMonitor.StartMonitoring() method to start the monitoring process.

The resulting method should look like this: public void StartMonitoringRegistry() { if (changeMonitor == null) { changeMonitor = keyFactory.GetChangeMonitor(rootPath); changeMonitor.Changed += OnSourceChanged; } changeMonitor.StartMonitoring(); } 4. Add the OnSourceChanged() method The OnSourceChanged() method will be called when anything under our registry key changes. It is
EntLibExtensibility HOLs Page 48

The OnSourceChanged() method will be called when anything under our registry key changes. It is responsible for invoking the SectionChanged event on the configuration source, providing the names of which sections changed. Due to the way registry monitoring has been implemented, we don't get any details about what exactly changed, so instead you'll simply include all the section names available when the registry changes. Create a private method: private void OnSourceChanged(object source, RegistryKeyChangedEventArgs e) First, collect the set of section names in the registry. Use the GetReadKeyForChanges method on the RegistryKeyChangedEventArgs parameter to get access to the registry key that changed. Walk through the Subkeys collection, and collect the names of all the keys in a list. Be sure to put the registry key into a using block. After having done this, invoke the SourceChanged event if it's not null, passing this and a new ConfigurationSourceChangedEventArgs with the configuration source and the list of sections as a parameter.
The resulting method should look like this: private void OnSourceChanged(object source, RegistryKeyChangedEventArgs e) { var sections = new List<string>(); using (var key = e.GetReadKeyForChange()) { sections.AddRange(key.Subkeys.Select(k => k.Name)); }

var sourceChanged = SourceChanged; if (sourceChanged != null) { sourceChanged(this, new ConfigurationSourceChangedEventArgs(this, sections)); }


}

5. Implement AddSectionChangedHandler and RemoveSectionChangedHandler In Enterprise Library versions prior to 5.0, there was no single event you could sign up for to get notifications for any section that had changed. Instead, you used the AddSectionChangedHandler() method to get a notification for a specific section and the corresponding RemoveSectionChangedHandler() to stop getting notifications. While no longer needed, these methods remain for backwards compatibility. You will be implementing these methods as wrappers around the new SourceChanged event. First, create a helper method named CreateSourceChangedHandler(). It should return EventHandler<ConfigurationSourceChangedEventArgs> and take two parameters: string sectionName and ConfigurationChangedEventHandler handler. Inside the method, return a lambda statement that takes two parameters, sender and e. Check
EntLibExtensibility HOLs Page 49

Inside the method, return a lambda statement that takes two parameters, sender and e. Check e.ChangedSectionNames to see if it contains the section name of interest, and if so invoke the handler parameter, passing the config source's this reference and a new ConfigurationChangedEventArgs object. In the AddSectionChangeHandler() method, call CreateSourceChangedHandler() and add the return value to the SourceChanged event. In the RemoveSectionChangeHandler() call CreateSourceChangedHandler() again and remove the result from the event. The resulting code should look like this: public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) { SourceChanged += CreateSourceChangedHandler(sectionName, handler); } public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) { SourceChanged -= CreateSourceChangedHandler(sectionName, handler); } private EventHandler<ConfigurationSourceChangedEventArgs> CreateSourceChangedHandler(string sectionName, ConfigurationChangedEventHandler handler) { return (sender, e) => { if (e.ChangedSectionNames.Contains(sectionName)) { handler(this, new ConfigurationChangedEventArgs(sectionName)); } }; }
6. Add the StopMonitoringRegistry method

There are times when you do not want to be monitoring the source, either due to overhead or permissions. Add a public method named StopMonitoringRegistry(). Check if changeMonitor is not null, and if not call the StopMonitoring() method on it. The resulting code should look like this: public void StopMonitoringRegistry() { if (changeMonitor != null) { changeMonitor.StopMonitoring(); } } 7. Clean up the change monitor at dispose time The change monitor creates a separate thread, and also keeps a registry key open. In order to be
EntLibExtensibility HOLs Page 50

The change monitor creates a separate thread, and also keeps a registry key open. In order to be sure these things are cleaned up, modify the implementation of the Dispose() method to check if there is a changeMonitor, and if so Dispose() it. public void Dispose() { if (changeMonitor != null) { changeMonitor.Dispose(); changeMonitor = null; } }
8. Start monitoring in the constructor Add a second constructor to the source that takes the IRegistryKeyFactory, the rootPath, a new boolean flag, shouldMonitorForChanges. If shouldMonitorForChanges is true, then call the StartMonitoringRegistry() method. Modify the original constructor so that it calls this new one passing true for shouldMonitorForChanges.

The resulting code should look like this: public RegistryConfigurationSource(IRegistryKeyFactory keyFactory, string rootPath) : this(keyFactory, rootPath, true) { } public RegistryConfigurationSource(IRegistryKeyFactory keyFactory, string rootPath, bool shouldMonitorForChanges) { this.keyFactory = keyFactory; this.rootPath = rootPath; if (shouldMonitorForChanges) { StartMonitoringRegistry(); } } 9. Run the tests Look at the IntegrationTests\ChangeMonitoringFixture.cs file in the test project. This includes a test that verifies that upon a change to the registry the change event fires. Run this test.

If you run into difficulties, you can look at the project in the <lab root>\RegistryConfigurationSource \Ex03\End directory for working code for this exercise.

EntLibExtensibility HOLs Page 51

Exercise 4 - Adding Config Tool Support

Introduction
Just like any other extension point in Enterprise Library, some extra work needs to be done to get custom configuration sources to work in the configuration tool. In this exercise, you'll provide those hooks and see the config tool read config information from and write config information to the registry. Estimated Time to Complete: 20 minutes

Exercise Instructions
1. Open the solution file Open the solution file at <lab root>\RegistryConfigurationSource\Ex04\Begin \RegistryConfigurationSource.sln in Visual Studio 2010. 2. Add base class and constructors to the RegistryConfigurationSourceELement class Open the Configuration\RegistryConfigurationSourceElement.cs file. There is a start of a class which will be the configuration element for our new configuration source. First, add a base class to the RegistryConfigurationSourceElement class. Configuration source config elements inherit from the ConfigurationSourceElement class. Next, add two constructors. The first should take two parameters, string name, and string registryKey. This constructor should invoke the base class constructor, passing the name and the type of the configuration source this element represents. Store the value of the registryKey parameter in the RegistryKey property (which hasn't been written yet, so dont worry about the red squiggles yet).

The second constructor should take no parameters, and invoke the other constructor, passing the string "Registry Configuration Source" for the name and string.Empty for the registryKey. The code should look like this:
public class RegistryConfigurationSourceElement : ConfigurationSourceElement { private const string registryKeyPropertyName = "registryKey"; private const string monitorChangesPropertyName = "monitorChanges";

public RegistryConfigurationSourceElement() : this("Registry Configuration Source", string.Empty) {


} public RegistryConfigurationSourceElement(string name, string registryKey) : base(name, typeof(RegistryConfigurationSource)) { RegistryKey = registryKey;
EntLibExtensibility HOLs Page 52

RegistryKey = registryKey;
} } 3. Add properties for RegistryKey and MonitorChanges

Next, add two properties to the class. RegistryKey is a string, and MonitorChanges is a bool. These will be the two configuration properties that can be set from the config file. You will need to do the usual stuff for configuration properties - add the ConfigurationProperty attribute, DisplayName and Description attributes. RegistryKey should be required, MonitorChanges should not be required and should default to true.
The code should look like:

[ConfigurationProperty(registryKeyPropertyName, IsRequired = true)] [DisplayName("Registry Key")] [Description("Registry key containing the configuration information")] public string RegistryKey { get { return (string) base[registryKeyPropertyName]; } set { base[registryKeyPropertyName] = value; } } [ConfigurationProperty(monitorChangesPropertyName, IsRequired = false, DefaultValue = true)] [DisplayName("Monitor for Registry Changes")] [Description("Monitor the registry key and fire change notifications if it changes")] public bool MonitorChanges { get { return (bool) base[monitorChangesPropertyName]; } set { base[monitorChangesPropertyName] = value; } } The two constants for the property names are already included in the class.
4. Implement the CreateSource and CreateDesignSource methods Configuration sources are used before the dependency injection container is configured, so they dont use the TypeRegistration system that other providers do. Instead, there are two methods you must implement: CreateSource() and CreateDesignSource(). For our purposes, they can both do the same thing: new up an instance of RegistryConfigurationSource, passing a new RegistryKeyFactory instance, the registry root key, and the monitor changes flag. Note: There are two methods for supporting configuration sources for non-Entlib configuration settings like AppSettings or ConnectionStrings. This source will not be needing that support.

The resulting code should look like:


public override IConfigurationSource CreateSource() { return new RegistryConfigurationSource(new RegistryKeyFactory(), RegistryKey, MonitorChanges); }

public override IDesignConfigurationSource CreateDesignSource( IDesignConfigurationSource rootSource) {


EntLibExtensibility HOLs Page 53

{
return new RegistryConfigurationSource(new RegistryKeyFactory(), RegistryKey, MonitorChanges);

}
5. Add class level attributes The final step in the configuration element is the element level attributes. Just like any other Entlib configuration element, you can add the DisplayName and Description attributes to make it look more intuitive in the config tool. For configuration sources, there're two other attributes that are helpful: Browsable(true) to make it show up in type browsers, and EnvironmentalOverrides(false). Add these attributes to the class. Note: EnvironmentalOverrides require support for storing multiple versions for each environment. Our source does not do this, so we don't support this feature.

The resulting code should look like:


[DisplayName("Registry Configuration Source")] [Description("Reads configuration information from a key in the Windows Registry")] [Browsable(true)] [EnvironmentalOverrides(false)] public class RegistryConfigurationSourceElement : ConfigurationSourceElement { 6. Hook up the configuration element to the configuration source class Just like any other provider, configuration sources require the ConfigurationElementType attribute so that Entlib knows how to load the type from configuration. Add the ConfigurationElementType attribute to the RegistryConfigurationSource class to hook up the configuration element. [ConfigurationElementType(typeof (RegistryConfigurationSourceElement))] 7. Add the IDesignConfigurationSource interface and implement it The configuration tool requires an additional interface, IDesignConfigurationSource , to be implemented by configuration sections. Add this interface to the RegistryConfigurationSource class.

The interface has four methods. They are:


void Add(string sectionName, ConfigurationSection section, string protectionProviderName) : this method supports saving encrypted configuration sections. This source will not support encryption, so simply throw a NotImplementedException for this method. ConfigurationSection GetLocalSection(string sectionName) : this method retrieves a section at design time. Implement it by calling the already written GetSection() method. void AddLocalSection(string sectionName, ConfigurationSection section) : this method adds a section to the configuration source at design time. Implement it by calling the already written Add() method.
EntLibExtensibility HOLs Page 54

calling the already written Add() method. void RemoveLocalSection(string sectionName) : this method removes a section at design time. Implement it by calling the already written Remove() method. The resulting code to implement this interface should look like:

public void Add(string sectionName, ConfigurationSection configurationSection, string protectionProviderName) { throw new NotImplementedException(); } public ConfigurationSection GetLocalSection(string sectionName) { return GetSection(sectionName); }
public void AddLocalSection(string sectionName, ConfigurationSection section) { Add(sectionName, section); }

public void RemoveLocalSection(string sectionName) { Remove(sectionName); }


8. Compile and Test Compile the project. Copy the resulting RegistryConfiguration.DLL to the <lab root>\Reference Assemblies\Entlib 5 directory. Then right click on the App.config file in the project and select "Edit Enterprise Library V5 Configuration". This should launch the configuration tool.

First, delete the Application Settings and Database Settings blocks that the tool automatically adds. If you do not do this, you will get an invalid cast exception when you try to save the file. In the tool, select the "Blocks|Add Configuration Settings" menu item to add the Configuration Sources to the configuration. Hit the "+" button to add a new configuration source. Your new Registry Configuration Source should appear in the menu.

EntLibExtensibility HOLs Page 55

Add the Registry Configuration Source. Set the Registry Key property to "HKCU\Software \Microsoft\Practices\EntlibWorkshop\DesignTimeTest". Next, click the chevron in the Configuration Sources bar to open up the general configuration sources properties. Change the "Selected Source" dropdown to "Registry Configuration Source".

Next, add a block or two to the configuration, for example, logging and exception handling. Now save the configuration. Open up regedit and look in HKEY_CURRENT_USER\Software\Microsoft\Practices \EntlibWorkshop\DesignTimeTest. You should see your configuration settings now stored in the registry.

EntLibExtensibility HOLs Page 56

If you run into difficulties, you can look at the project in the <lab root>\RegistryConfigurationSource \Ex04\End directory for working code for this exercise.

EntLibExtensibility HOLs Page 57

Resources & contacts


patterns & practices dev center http://msdn.microsoft.com/practices EntLib's site on MSDN: http://msdn.microsoft.com/entlib List of EntLib's extensibility points: http://cut.ms/2wE Discussions, learning resources and support: http://entlib.codeplex.com http://unity.codeplex.com

Enterprise Library community extensions: http://entlibcontrib.codeplex.com


Developer's Guide to Enterprise Library: http://go.microsoft.com/fwlink/?LinkId=188937 EntLib producer: Grigori: http://blogs.msdn.com/agile @gmelnik EntLib dev lead: Chris: www.tavaresstudios.com/Blog

EntLibExtensibility HOLs Page 58

Anda mungkin juga menyukai