Sunday, March 04, 2007 7:24 PM
Olaf Conijn
Using the Policy Injection Application Block
Since Both Tom and Ed wrote an article explaining whatever the Policy Injection Application Block [PIAB] is, you might be interested in how to use this new block from within Enterprise Libraries February CTP. Tom, who is the Product Manager on Enterprise Library, has been keeping me fairly busy over the last couple of days and helped out posting some of the OPAB Configuration schema already.
So let’s start with this exercise in using the Policy Injection Application Block!
Since I wouldn’t know about your real world application (though feel free to post specific problems you run into on the forums!) I created my own “real world” application. An ATM (or Automated Teller Machine):
As most other real world application this application is about more than just decreasing the balance after a withdrawal is made. The requirements include:
- Log all operations to a text file.
- Log all withdrawals of $1000 or above to the eventlog (the business guys refer to these withdrawals as “Large Withdrawal”).
- No withdrawal can be made that exceeds the customer’s credit limit
- Exceptions that occur, should be handled gracefully
Now, let’s dive into the code and have a look at how this Policy Injection thing will help us out.
(The complete project can be found as an attachment, the complete listing of PIAB configuration can be found at the bottom of this blogpost)
Defining the “Interceptable” interface
Since we will be intercepting method calls within the application we first need to determine what methods we would be potentially interested in to intercept. This can be done in 2 ways:
1) If we want to have the system intercept *all* calls to a specific class (and we have the freedom of defining our own base class on this class) we can do so by having this class derive from MarshalByRefObject.
2) If we want a more fine grained interface to do interception on (or we already have a baseclass, other than MarshalByRefObject) we can have our class implement an interface, which will define the methods we can do interception on.
Additionally, all classes that have methods we would like to intercept need to be created using a factory, called PolicyInjection. In code this looks like:
//All methods on this class can be intercepted, it derives from MarshalByRefObject
public class InterceptAllMyMethods : MarshalByRefObject
{
public void Method1() { }
public void Method2() { }
}
(then, when creating an instance of the class above)
//Have the factory create the instance, this will make interception possible
InterceptAllMyMethods interceptable1 = PolicyInjection.Create<InterceptAllMyMethods>();
interceptable1.Method1(); //this method call will be intercepted by the PolicyInjection AB
or:
//Methods defined on IInterceptableMethods can be intercepted
public class InterceptMethodsInInterface : IInterceptableMethods
{
public void Method1() { }
public void Method2() { } //not in interface, will not be intercepted
}
public interface IInterceptableMethods
{
void Method1();
}
(again, create the object using the factory)
IInterceptableMethods interceptable2 = PolicyInjection.Create<InterceptMethodsInInterface, IInterceptableMethods>();
interceptable2.Method1(); //this method call will be intercepted by the PolicyInjection AB
In our teller machine we choose the second option, and defined an interface that looks like the following:
public interface ITeller
{
void WithdrawAmount20(Account account);
void WithdrawAmount50(Account account);
void WithdrawAmount100(Account account);
void WithdrawAmount500(Account account);
[Tag("Large Withdrawal")]
void WithdrawAmount1000(Account account);
[Tag("Large Withdrawal")]
void WithdrawAmount2500(Account account);
void Kicked();
}
When creating an instance of the Teller class (which derived from this interface) we do this using the PolcyInjection factory. This will make every method call in the interface about a possible victom for interception.
teller = PolicyInjection.Create<Teller, ITeller>();
Now this is done, we can proceed with our first requirement:
Logging every operation to a text file
Since we have our instance of the Teller class created using the PolicyInjection factory, next thing we need to think about is what to do in order to satisfy this requirement.
Every operation in this requirement can be interpreted as “all method calls to the ITeller interface” (above). Logging this to a textfile, in our context can be interpreted as use Enterprise Library logging to write the event to a text file.
These 2 bits of information (the where and the what) make up a Policy. This Policy can be configured and will be Injected into the Teller class (without the need of explicitly writing the code to call The Logging Application Block, over and over again).
The generic skeleton for a policy in configuration looks like the following:
<policies>
<add name="PolicyName">
<matchingRules>
[List of rules that should evelautate true,
before policy is applied]
</matchingRules>
<handlers>
[List of handlers that should be invoced
by this Policy]
</handlers>
</add>
</policies>
For this specific policy we add:
1) A rule to the matchingRule-element that will match any method on the ITeller interface
2) A handler that calls into the Logging Application block, to update the textfile, we use as a log.
In the applications configuration this now looks like the following:
<add name="LogAllOperationsToFile">
<matchingRules>
<add
name="Type is ITeller"
type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.TypeMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection"
ignoreCase="false"
match="ITeller" />
</matchingRules>
<handlers>
<add
name="Log operation to text file"
type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.LogCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers"
logBehavior="Before"
beforeMessage="This is the before message"
afterMessage="This is the after message"
includeParameterValues="true"
includeCallStack="true"
includeCallTime="false"
priority="42"
severity="Information">
<categories>
<add name="OperationOnTeller" />
</categories>
</add>
</handlers>
</add>
Every Handler or MatchingRule has its own set of configuration settings.
As a Rule we use the TypeMatchingRule and we configure it to match the ITeller type.
As a handler we use the LogCallHandler and do this in a verbose way (it will log incoming parameter values, as well as the current callstack). The category OperationOnTeller will be passed to the Logging Application Block, together with the information about the specific call. This category is configured to log to a textfile.
Believe it or not (and if you don’t, the application attached will prove it) but from this point on all calls to the ITeller interface will be logged to a textfile (trace.log, in the applications run directory).
Next up is Requirement #2:
Large transactions should be logged to the eventlog
You might well have noticed that the methods on the ITeller interface for a Transfer of $1000 and $2500 have an attribute on them.
[Tag("Large Withdrawal")]
void WithdrawAmount1000(IPrincipal authenticatedUser);
[Tag("Large Withdrawal")]
void WithdrawAmount2500(IPrincipal authenticatedUser);
PIAB allows us to configure a MatchingRule (similar to the one we specified to match all operations on the ITeller interface) based on these tag-values. This comes in handy with this second requirement, since it allows us to explicitly annotate the methods we are interested in.
Similar to the implementation of our first requirement, we set up a Policy in configuration that will call the Logging application block to do work when a method is called. This time around we specify a TagAttributeMatchingRule in the matchingRules element. Have a look at its definition in config:
<add name="LogLargeWithdrawalsToEventLog">
<matchingRules>
<add name="Tag is Large Withdrawal" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.TagAttributeMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="Large Withdrawal" />
</matchingRules>
<handlers>
<add name="Log operation to text file" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.LogCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers, Version=2.9.9.2" logBehavior="Before" beforeMessage="Large withdrawal has been made" includeParameterValues="true" severity="Information">
<categories>
<add name="LargeWithdrawal" />
</categories>
</add>
</handlers>
</add>
Again the category “LargeWithdrawal” is configured inside the logging application to write messages to the eventlog.
(I left out some of the optional configuration settings on the logging-handler, the previous snippet contains a more complete list of possibilities)
Enough of Logging? Next up is Validation
No withdrawal can be made that exceeds the customer’s credit limit
Since the February CTP only supports validation incoming parameter instances as a whole (support to validate a parameter using attributes put on the parameter declaration will be added in the final release) we need to find a good place to inject this validation behavior.
In our sample application there is a class called Account, which has a method WithdrawFunds:
public class Account : MarshalByRefObject
{
public void WithdrawFunds(Withdrawal withdrawal)
{
AccountData accountData = AccountRepository.RetrieveAccount(customerName);
accountData.Balance -= withdrawal.Amount;
accountData.TransactionHistory.Add(new TransactionHistoryData(withdrawal.Amount, DateTime.Now));
}
The Withdrawal class is annotated with attributes to do validation on whether the Withdrawal doesn’t exceed the customer’s credit limit. So we decide to intercept this method and validate its parameter, again using PIAB configuration.
Also note that this class derives from MarshalByRefObject and we don’t need to define an interface to do interception. When creating an instance of Account, we simply call the Factory, passing it the constructor arguments:
account = PolicyInjection.Create<Account>(Environment.UserName);
In configuration there is no need to apply this policy on all methods declared on Account, only intercepting the WithdrawFunds method will do.
We decide to do this with a combination of MatchingRules, the NamespaceMatchingRule, TypeMatchingRule and MemberNameMatchingRule can be combined to match our WithdrawFunds- method (without matching other methods, called ‘WithdrawFunds’).
Combining MatchingRules can be done by adding all of them inside the matchingRule element together. These combined MatchingRules are all evaluated and only methods that match all of them are intercepted.
As the Handler for this Policy we configure the ValidationCallHandler, which will validate all parameters passed to the intercepted method. We configure this instance of the ValidationCallHandler to use the default RuleSet (this is done by leaving the ruleSet attribute empty) and set the specificationSource-attribute to Attributes, since the validation logic on our Withdrawal class is implemented using attributes (other flavors we’ve got are: Configuration or Both).
<add name="ValidateWithdrawalsOnTeller">
<matchingRules>
<add name="Namespace is AutomatedTeller.Logic" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.NamespaceMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="AutomatedTeller.Logic" />
<add name="Type is Account" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.TypeMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="Account" />
<add name="Member is WithdrawFunds" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.MemberNameMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="WithdrawFunds" />
</matchingRules>
<handlers>
<add name="Validate Parameters" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.ValidationCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers" ruleSet="" specificationSource="Attributes" />
</handlers>
</add>
Having all of this set up properly, the ValidationCallHandler throws an exception of the ArgumentValidationException when the input to our method is invalid. This leads us to implementing our last requirement:
Exceptions that occur, should be handled gracefully
When customers hit their credit limit, they occasionally become aggressive and start kicking the machine. Now -don’t start slapping your computer around! –there is a “Kick machine” button on the applications UI which can be used to simulate this.
Both the “Kick machine” button and validation-failure of a withdrawal (when exceeding credit limit) causes an exception to be thrown. We already have the Exception Handling block and configuration in place to deal with these exceptions, though we want to use the PIAB to call into the Exception Handling block.
In our example the Exception Handling Block is used to translate these unfortunate events into Exceptions with messages proper to show in the UI. Since all calls into our business logic flow through the ITeller interface, we decide that would be the place to intercept calls and when intercepting we have the ExceptionCallHandler call into the Exception Handling Block.
Again, in our policy-configuration we match to a combination of MatchingRules (to be more concise in where we would like to intercept): Namespace is AutomatedTeller.Logic and Type is ITeller.
The ExceptionCallHandler is configured to call into the Exception Handling block, using the Exception-policy UI Policy.
The final piece of PIAB Configuration we add to our app.config then looks like the following:
<add name="TranslateExceptionsForUI">
<matchingRules>
<add name="Namespace is AutomatedTeller.Logic" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.NamespaceMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="AutomatedTeller.Logic" />
<add name="Type is ITeller" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.TypeMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="ITeller" />
</matchingRules>
<handlers>
<add name="UI ExceptionHandler Policy"
type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.ExceptionCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers" exceptionPolicyName="UI Policy"/>
</handlers>
</add>
Since our application now complies with the requirements defined for it to run, I’d suggest we send the whole lot to test. Our job here seems done; we implemented the requirement for this application using configuration and the Policy Injection Block.
Additional information
The Policy Injection Application Block is still not finished, nor is the public interface (or configuration) fixed. These could well change before we release the final version. Also there are some known issues with this release of PIAB; still it should give you a fair idea of how this is envisioned to work.
The list of MatchingRules you can with in this CTP is:
-
AssemblyMathingRule (matches methods in a specific assembly)
-
CustomAttributeMatchingRule (matches methods with an arbitrary attribute)
-
MemberNameMatchingRule (matches a method by its name)
-
MethodSignatureMatchingRule (matches a particular overload of a method)
-
NamespaceMatchingRule (matches methods in a specific namespace)
-
ReturnTypeMatchingRule (matches methods with a specific return type)
-
TagAttributeMatchingRule (matches methods with the TagAttribute and a specific tag-value)
-
TypeMatchingRule (matches methods on a specific type)
The list of Handlers in this CTP is (we should be familiar with these now):
There are stubs for the following Handlers (which won’t work, but it seems a fair assumption that these will be included in the final releaseJ):
-
PerformanceCounterCallHandler
Increments performance counters when a method is intercepted
-
CachingCallHandler
Caches the output of a method and short circuits the calls if the return value was cached previously
-
AuthorizationCallHandler
Calls a configured authorization provider and does an access-check before the method is being executed, either throwing a UnAuthorizedException or proceeding with the invocation
The PIAB handlers are executed in the order at which they appear in configuration. This allows you to control, which is done first: caching or authorization.
Caching the authorization logic does not make a lot of sense, does it? ;-)
For ObjectBuilder users there is an additional method of creating an interceptable proxy (and using this application block in their own frameworks). This can be done using the PolicyInjectionStrategy and PolicyInjectionPolicy. If you’re an OB-geek (such as myself) you probably know what to do with those.
Good luck having your own way with this new block. For questions, comments and bugs you run into you can use the communities’ forum at: www.codeplex.com/entlib
The complete PIAB configuration we used in this sample is as follows:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="policyInjection" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.Configuration.PolicyInjectionSettings, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" />
</configSections>
<policyInjection>
<policies>
<add name="LogAllOperationsToFile">
<matchingRules>
<add name="Type is ITeller" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.TypeMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=2.9.9.2" ignoreCase="false" match="ITeller" />
</matchingRules>
<handlers>
<add name="Log operation to text file" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.LogCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers, Version=2.9.9.2" logBehavior="Before" beforeMessage="This is the before message" afterMessage="This is the after message" includeParameterValues="true" includeCallStack="true" includeCallTime="false" priority="42" severity="Information">
<categories>