Implementing Business Rules Using Custom Attributes


To work through this example, you will need to complete the coding up through the first part of Chapter 8, "Reusing Code." When you are done with this example you will have created a set of classes that you can reuse in your own projects to implement business rules.

Note

I found the basis for this code online at the Newtelligence AG company (http://www.newtelligence.com), which built this code in C# as the basis for a Web security interface application. I converted this code to Visual Basic and enhanced it to fit within the framework of the application you have been creating in this book. The code from AG New Intelligencer was developed under a BSD-style license and is used here with the author's permission (Clemens F. Vasters, who can be reached at <clemensv@newtelligence.com>). Although the code presented here is different from the original code, the implementation of this idea came from the original code.

You can extend this small amount of code, which needs to be written only once, to fit virtually any type of business rule that you may need to create.

Note

When I originally came across this code, I was thinking of having a class of business rules usable by an entire organization. In this manner, no one in an organization would ever need to build the basic set of business rules ever again, and everyone would have access to these rules.

Creating the BusinessRules Project

You are going to create a separate project to hold all of the business rule attributes and the validation routines. In this way you can distribute the rules to other applications. This project will be another shared project that must exist on both the client and the server. The reason for this is that the class attributes you create will be used in both the data-centric and user-centric objects.

To begin, open the current Northwind solution, add a new Class Library project to the solution, and call it BusinessRules. Rename the Class1.vb file that is created by default to Attributes.vb. Then delete the default class definition that was created in this code module. You will create the business rule attributes and the necessary interface in this code module. You will eventually create another set of classes to check the business rules specified by the attributes.

Add the following code to the Attributes code module:

 Option Explicit On Option Strict On Imports System.Reflection Namespace Attributes End Namespace 

The interface and all of the classes you create will be created in the Attributes namespace. Before you begin adding classes, let's review the business rules in place in the RegionDC class:

  • RegionDescription cannot be null.

  • RegionDescription cannot be a zero-length string.

  • RegionDescription cannot be more than 50 characters in length.

This gives you the basis for creating your first set of class attributes.

Note

These are the only attributes you will be creating for this project; however, in the code available for download, there are a considerable number of additional business rule attribute classes.

Going by this list of rules, you need to create three attribute classes that check for the following: a null value, an empty length string, and a maximum number of characters.

Creating the ITest Interface

Before creating the classes, you need to create an interface that all of your classes will support.

Note

You need the interface because these are all generic classes. When you code the routines that check the rules, you will see that you do not care what the attribute class is, only that it is a rule and that you need to check the rule. In this way, you can continue to add additional business rules without once having to change the way in which you check the rules.

Add the code for the ITest interface as shown in Listing 10-8 to the Attributes namespace in the Attributes code module.

Listing 10-8: The ITest Interface

start example
 Public Interface ITest      Function TestCondition(ByVal Value As Object, ByRef cls As Object) As Boolean      Function GetRule() As String End Interface 
end example

The TestCondition method actually determines if the value has broken the specific business rule. It accepts the value stored in the field or property and the object in which the property resides. This is enough information for a method to determine everything about a given class. It returns a value of True if the rule has been broken and a value of False if the rule has not been broken. The GetRule method simply returns a string that describes the rule in plain English. This will be used (in conjunction with another method) to eliminate the need for all of the code in the GetBusinessRules method.

Creating the NotNullAttribute Class

Now you can create the first business rule attribute class: NotNullAttribute. Add the code for the NotNullAttribute class as shown in Listing 10-9.

Listing 10-9: The NotNullAttribute Class

start example
 <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property)> _ Public Class NotNullAttribute      Inherits System.Attribute      Implements ITest      Public Function TestCondition(ByVal Value As Object, ByRef cls As Object) _      As Boolean Implements ITest.TestCondition           If Value Is Nothing Then                Return True           Else                If IsNumeric(Value) Then                     If Convert.ToDecimal(Value) = 0 Then                          Return True                     End If                End If                Return False           End If      End Function      Public Function GetRule() As String Implements ITest.GetRule           Return "Value cannot be null."      End Function End Class 
end example

Let's examine this code to determine exactly what is happening. The signature tells you that this class can only be applied to a field or property within a class.

 <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property)> _ Public Class NotNullAttribute 

As before, all classes that are attribute classes must inherit from the System.Attribute class. Next, your class implements the ITest interface as will all of your attribute classes. Now you come to the TestCondition method, which does the real work of the class. This first check just tests to see if the value is null; if it is, it returns True and the method ends:

 If Value Is Nothing Then      Return True Else 

The second part of this routine may or may not be controversial. Because numbers are not nullable, when they are instantiated they are initialized with a value of zero. If a numeric value can be a zero, you should not apply this attribute to it because this attribute is supposed to deal with nulls and is named accordingly, but for simplicity it is useful to keep it in this class. You can always create a separate class called ValueNotZeroAttribute and add this code into it—the choice is yours. This code checks to see if the value is numeric, and if it is, it checks to see if the value is equal to zero:

 If IsNumeric(Value) Then      If Convert.ToDecimal(Value) = 0 Then           Return True      End If End If Return False 

The last method in the class, the GetRule method, simply returns what the rule for the property is.

Creating the DisplayNameAttribute Class

Now, you have one small problem here—how do you show the property to the user in a way that looks nice to the user? If you go by just the name of the property, it is going to look ugly because there are no spaces and sometimes property names do not reflect what the user sees on the screen. To overcome this you are going to add another class called DisplayNameAttribute that will store the name for the property you want to show the user.

Add the DisplayNameAttribute class as shown in Listing 10-10.

Listing 10-10: The DisplayNameAttribute Class

start example
 <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property)> _ Public Class DisplayNameAttribute      Inherits System.Attribute      Private _strValue As String      Public Sub New(ByVal Value As String)           _strValue = Value      End Sub      Public ReadOnly Property Name() As String           Get                Return _strValue           End Get      End Property End Class 
end example

Creating the NotEmptyAttribute Class

Now you will create the rule that will check to make sure that a string value is not empty. Listing 10-11 shows the code for this class.

Listing 10-11: The NotEmptyAttribute Class

start example
 <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property)> _ Public Class NotEmptyAttribute      Inherits System.Attribute      Implements ITest      Public Function TestCondition(ByVal Value As Object, ByRef cls As Object) _      As Boolean Implements ITest.TestCondition           If Value Is Nothing Then                Return True           Else                Dim str As String = CType(Value, String)                If str.Trim.Length = 0 Then                     Return True                Else                     Return False                End If           End If      End Function      Public Function GetRule() As String Implements ITest.GetRule           Return "Value cannot be a zero length string."      End Function End Class 
end example

Everything that is occurring in this class should be straightforward except for the check to see if the value is nothing. This check must be made in some form or another in every class that checks a property. After all, how can you check the value of something if the value is nothing? Notice also how similar this is to the first attribute class you created. The beauty of creating rules this way is that the code is compact, easy to understand, and even easier to debug. And once you get it right here, you never need to check it again or write code to perform the same type of validation.

Creating the MaxLengthAttribute Class

This last attribute class is substantially identical to the previous two business rule attributes that you created. Listing 10-12 presents the code for this class.

Listing 10-12: The MaxLengthAttribute Class

start example
 <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property)> _ Public Class MaxLengthAttribute      Inherits System.Attribute      Implements ITest      Private _intValue As Integer      Public Sub New(ByVal Value As Integer)           _intValue = Value      End Sub      Public Function TestCondition(ByVal Value As Object, ByRef cls As Object) _      As Boolean Implements ITest.TestCondition           Dim strValue As String = Convert.ToString(Value)           If strValue.Length > _intValue Then                Return True           Else                Return False           End If      End Function      Public Function GetRule() As String Implements ITest.GetRule           Return "Value cannot be longer than " & _intValue & " characters."      End Function End Class 
end example

This class simply checks the length of a string value to determine if it has more characters than allowed. Notice that your GetRule method now incorporates the value that you set into the string that is returned. In this case, the RegionDescription property would return a rule that said, "Region Description cannot be longer than 50 characters."

Now that you have all of the business rules in place, you can apply them to your object.

Assigning Data-Centric Business Rule Attributes

To begin, right-click the NorthwindDC references node and select Add Reference. From the Projects tab, select the BusinessRules project by double-clicking it and then click OK. Next, switch to the RegionDC class, expand the Public Attributes region, and delete the public RegionDescription property. In a single move you have eliminated 19 lines of code from your project (yes, it is at the expense of adding all of the code for the business rule attributes, but think about it, you never need to add them again and this is not all of the code you will delete). Expand the Private Attributes region and change the mstrRegionDescription variable to the following:

 Public RegionDescription As String 

Next, go to the Save method and change the mstrRegionDescription variable to RegionDescription. Before you apply the attributes, you have to import the BusinessRules.Attributes namespace; once that is done you can start applying attributes. So, add the line to perform this to the top of the RegionDC module.

Now you need to apply the attributes. Change the RegionDescription declaration line so that it reads as follows:

 <DisplayName("Region Description"), NotNull(), NotEmpty(), MaxLength(50)> _ Public RegionDescription As String 

It may be anticlimactic, but in reality an easy-to-maintain system does not throw many complicated surprises at you! These three tags tell your class what the value cannot be. The DisplayName attribute tells you the human readable name you will display to the user.

Retrieving the List of Business Rules

Before you get into retrieving the business rules, you need to make one change to your application. You need to move your BusinessErrors class and your structErrors structure to the BusinessRules project. The reason you need to do this is so that your new attribute class is modular and can be used by other applications. Follow these steps to accomplish this:

  1. Add a new class module to the BusinessRules project called Errors.vb.

  2. Delete the default class that is created in this code module.

  3. Add a namespace in the Errors code module called Errors (this will now be referenced by using BusinessRules.Errors).

  4. Cut the BusinessErrors class from the NorthwindShared Errors code module and paste it into the Errors namespace in the BusinessRules project.

  5. Cut the structErrors structure from the NorthwindShared Structures code module and paste it into the Errors namespace in the BusinessRules project.

  6. Add a reference to the BusinessRules project in the NorthwindUC, NorthwindShared, and NorthwindTraders projects.

  7. In each edit form code module, all the data-centric and all the user-centric code modules, as well as the NorthwindShared Interfaces code module and the frmBusinessRules module, replace northwindshared.errors and northwindtraders.northwindshared.errors with businessrules.errors.

As you perform steps 4 and 5 of this list, you will see several errors in the Task List. Not to worry, though—once you are through with the last step, you will not have any errors. That was the hardest part. The next step is to add a new class code module to the BusinessRules project called Validate. Once you have done that, delete the default class and add the following code to the Validate class module:

 Option Explicit On Option Strict On Imports BusinessRules.Attributes Imports BusinessRules.Errors Imports System.Reflection Namespace Validate     Public Class Validation     End Class End Namespace 

The Validation class is the only class you are going to create in this namespace. Now, add the GetDisplayName method to the Validation class as shown in Listing 10-13.

Listing 10-13: The GetDisplayName Method

start example
 Private Shared Function GetDisplayName(ByVal member As MemberInfo) As String      Dim obj() As Object = member.GetCustomAttributes(True)      Dim i As Integer      If obj.Length > 0 Then           For i = 0 To obj.Length - 1                If TypeOf (obj(i)) Is DisplayNameAttribute Then                     Dim objDisplayNameAttribute As DisplayNameAttribute = _                    CType(obj(i), DisplayNameAttribute)                     Return objDisplayNameAttribute.Name                End If           Next i      End If      Return member.Name End Function 
end example

Let's examine what this code does, line by line. One important thing to note is the method signature. This is a shared method because it will be called by a shared method. In fact, all of the methods in this class will be shared methods so this class never has to be instantiated. This provides you with immense speed benefits (especially when you move to the user-centric classes), and because this class maintains no state at all, it is OK to do.

The first line retrieves a list of all of the custom attributes associated with the class member. The True parameter tells the code to retrieve all of the custom attributes along the entire inheritance chain:

 Dim obj() As Object = member.GetCustomAttributes(True) 

Next, you check to see if there are any custom attributes:

 If obj.Length > 0 Then 

If there are, you loop through them looking for an attribute that is a DisplayNameAttribute:

 If TypeOf (obj(i)) Is DisplayNameAttribute Then 

If we find one, you convert it to a true DisplayNameAttribute object and you return the value of the Name property:

 Dim objDisplayNameAttribute As DisplayNameAttribute = CType(obj(i), _ DisplayNameAttribute) Return objDisplayNameAttribute.Name 

Finally, if there was no Display Name attribute found, you simply return the name of the property. Now that you can return the display name, it is time to be able to return the rules.

Add the code for the GetBusinessRules method to the Validate class as shown in Listing 10-14.

Listing 10-14: The GetBusinessRules Method

start example
 Public Shared Function GetBusinessRules(ByVal cls As Object) As BusinessErrors      Dim t As Type = cls.GetType      Dim m As MemberInfo() = t.GetMembers      Dim i As Integer      Dim objBusErr As New BusinessErrors      For i = 0 To m.Length - 1           Dim obj() As Object = m(i).GetCustomAttributes(True)           If obj.Length > 0 Then                Dim j As Integer                For j = 0 To obj.Length - 1                     If TypeOf obj(j) Is ITest Then                          Dim objI As ITest = CType(obj(j), ITest)                          objBusErr.Add(GetDisplayName(m(i)), objI.GetRule)                     End If                Next           End If      Next      Return objBusErr End Function 
end example

This is the first routine where you access the ITest interface, so you will see the workings of this method line by line. The method accepts an object, which is the class you want to get the business rules from, and it returns a BusinessErrors object. The first line retrieves all of the type information about the class. The second line retrieves all of the members of the class and stores them in an array of MemberInfo objects:

 Public Shared Function GetBusinessRules(ByVal cls As Object) As BusinessErrors      Dim t As Type = cls.GetType      Dim m As MemberInfo() = t.GetMembers 

Next you loop through all of the members of the class and you retrieve the custom attributes of each member (you retrieve all of the custom attributes along the inheritance chain). This allows your inherited classes to use the business rules of any base classes. There may be a point at which this is not desirable, though, so you may need to modify this code to suit your particular needs. Then you check to see if there were in fact any custom attributes retrieved from the member:

 For i = 0 To m.Length - 1      Dim obj() As Object = m(i).GetCustomAttributes(True)      If obj.Length > 0 Then 

Finally, you loop through all of the custom attributes associated with the member. You check to see if the custom attribute implements the ITest interface, and, if it does, you call the GetRule method on it to retrieve the rule. You also extract the Display Name from the member and add them both to the BusinessErrors object:

 For j = 0 To obj.Length - 1      If TypeOf obj(j) Is ITest Then           Dim objI As ITest = CType(obj(j), ITest)           objBusErr.Add(GetDisplayName(m(i)), objI.GetRule)      End If Next 

Now that you have added this method, let's implement it. In the RegionDC class, add the following declaration:

 Private mobjVal As BusinessRules.Validate.Validation 

Alter the GetBusinessRules method so that it now reads as follows:

 Public Function GetBusinessRules() As BusinessErrors _ Implements IRegion.GetBusinessRules         Return mobjVal.GetBusinessRules(Me) End Function 

Now, build the application, but when you go to deploy the remote assemblies, be sure to deploy all three assemblies: NorthwindDC, NorthwindShared, and BusinessRules. Then run the application, go to Maintenance Regions, and select one of the existing regions to edit (or select the Add button). Then click the Rules button, and you should see the rules screen with your three business rules. Any changes you make to the business rules will be automatically reflected the next time the application is compiled and run, and you never need to change the GetBusinessRules method again.

Checking Business Rules with Custom Attributes

Now that you can retrieve the business rules, it is time to put them to their real use—constraining data. You will eventually end up writing two different methods to perform this task: one for the data-centric classes and one for the user-centric classes. They are different methods because they do the task in slightly different ways. However, you are only going to worry about the data-centric classes right now. You are going to add a new method to the Validation class (shown in Listing 10-15). This method is a little more involved than the GetBusinessRules method because you have to take into account the differences between fields and properties; but for the most part there are not a lot of differences between this method and the GetBusinessRules method.

Listing 10-15: The Validate Method

start example
 Public Shared Function Validate(ByVal cls As Object) As BusinessErrors      Dim t As Type = cls.GetType      Dim i, j As Integer      Dim bln As Boolean      Dim objBusErr As New BusinessErrors      Dim m As MemberInfo() = t.GetMembers      For i = 0 To m.Length - 1           Dim objAttrib() As Object = m(i).GetCustomAttributes(True)           For j = 0 To objAttrib.Length - 1                If TypeOf objAttrib(j) Is ITest Then                     Dim objI As ITest = CType(objAttrib(j), ITest)                     If TypeOf m(i) Is FieldInfo Then                          Dim fld As FieldInfo = CType(m(i), FieldInfo)                          bln = objI.TestCondition(fld.GetValue(cls), cls)                     End If                     If TypeOf m(i) Is PropertyInfo Then                          Dim pro As PropertyInfo = CType(m(i), PropertyInfo)                          bln = objI.TestCondition(pro.GetValue(cls, Nothing), _                          cls)                     End If                     If bln Then                          objBusErr.Add(m(i).Name, objI.GetRule())                     End If                End If           Next      Next      Return objBusErr End Function 
end example

The real difference in this listing is the test to determine if the member is a field or a property. The reason for this test is that the methods for retrieving the instance values are different for each type. This is because a property is a method, so it does not just retrieve a value; it actually invokes the method to return a value. You then call the TestCondition method, and if the value breaks the rule you add it to the BusinessError method.

One thing to note about this method of validating business rules is that all of the rules for each property will be checked as opposed to what you had before. Before only one rule at a time was being checked and thrown as an error. So this method provides you with a little more robust business rule handling and reporting.

start sidebar
Code Reduction Metrics

Many companies today are trying to take a cost-conscious approach to coding—so the first question asked when faced with a new technology is, "How much effort (read: money) will this save and how much easier is it to maintain?" If anyone was looking for a justification to use the .NET Framework, this is it.

On average, you can assume that you will save six lines of code for every business rule that is checked via a custom attribute as opposed to the previous method you were using. This should be able to help you extrapolate out the cost in savings by using custom attributes. In your RegionDC class, for example, you originally had 20 lines of code for the public RegionDescription property, one line for the private RegionDescription field, and nine lines of code for the GetBusinessRules. Now we have one line of code for the public RegionDescription field and three lines of code for the GetBusinessRules method. Thirty lines down to four is a big improvement.

A larger example is your EmployeeDC class. It has 18 properties, which have approximately 518 lines of code devoted to the public attributes plus another 26 lines of code devoted to the GetBusinessRules method. Using reflection, you can knock the number of lines of code down to 21—18 for the properties and three for the GetBusinessRules method. That is 544 lines of code knocked down to just 21 lines of code.

Furthermore, your objects are now truly self-describing. Any changes you make to your business rules are now instantly reflected when you retrieve the business rules from the class. The code reduction plus the self-describing class means that your maintenance costs will go through the floor. No longer do developers have to hunt through the code looking for the rule—they just have to check the attribute tag. Also, this reduces the number of code defects caused by bad business rule checks. Because your business rules are encapsulated, if they are wrong in one place, they are wrong in every place, and it will be much easier to capture these defects and correct them.

With all of the wonderful things that reflection provides, you may be asking yourself at this point why you saw the original method for handling business rules at all. After all, what is the point because this is so much easier and provides so many more advantages? The reason is that this is not a one-size-fits-all solution. On several occasions I have had to create systems that use a rules database because the business rules changed so quickly. In cases such as this, the objects generally need to open up a connection to a database to read the rule information. This is a lot of overhead and in the few tests that I have run is not well served by the reflection model. The reason for this is that the attributes cannot be dynamically changed at runtime by reading from a database. So, it is best to know both methods and apply them as necessary.

end sidebar

There is one last change you need to make to the RegionDC class—it is a change to the Save method. Currently, the first part of your Save method looks like the following:

 mobjBusErr = New BusinessErrors With sRegion      Me.mintRegionID = .RegionID      Me.RegionDescription = .RegionDescription End With 

The change you need to make is simple. Delete the first line from the previous code, and add the following line of code below the With block:

 mobjBusErr = mobjVal.validate(Me) 

Now, after all of your properties are assigned, you call the Validate method, retrieve the business errors, and continue as before.

Implement User-Centric Business Rule Attribute Classes

Checking business rules with custom attributes is a little different on the user-centric side. The reason for this is that you check the rules one property at a time. Not only do you still need to throw an exception when an error occurs, but you also need to add an entry to the BrokenRules object. That is a lot more work than you had to do in the data-centric class. Specifically, you cannot get rid of the public properties in the user-centric class like you did in the data-centric class. However, your job is made much easier by the presence of the BusinessBase class.

Before you modify your user-centric classes, let's add a new method to the Validation class. This method throws an exception on the first broken rule it encounters. Add the method shown in Listing 10-16 to the Validation class.

Listing 10-16: The ValidateAndThrow Method

start example
 Public Shared Sub ValidateAndThrow(ByVal cls As Object, ByVal field As String)      Dim t As Type = cls.GetType      Dim m As MemberInfo() = t.GetMember(field)      Dim i As Integer      Dim bln As Boolean      Dim obj() As Object = m(0).GetCustomAttributes(True)      If obj.Length > 0 Then          For i = 0 To obj.Length - 1                If TypeOf obj(i) Is ITest Then                     Dim objI As ITest = CType(obj(i), ITest)                     If TypeOf m(0) Is FieldInfo Then                          Dim fld As FieldInfo = CType(m(0), FieldInfo)                          bln = objI.TestCondition(fld.GetValue(cls), cls)                     End If                     If TypeOf m(0) Is PropertyInfo Then                          Dim pro As PropertyInfo = CType(m(0), PropertyInfo)                          bln = objI.TestCondition(pro.GetValue(cls, Nothing), cls)                     End If                     If bln Then                          Throw New Exception(objI.GetRule())                     End If                End If           Next      End If End Sub 
end example

This code is similar to what you have seen before, with the exception that when a broken rule is encountered, an exception is thrown. Notice that it does not specify the property that the exception is thrown on—you know what it is because you had to pass the property into the method. Notice also at the top of the method that you are only retrieving the information for the one property or field that you specified, not for the whole class. That is the extent of this method; now you can implement it in the BusinessBase class.

To begin with, modify the BusinessBase class by adding the following declaration:

 Protected mobjVal As BusinessRules.Validate.Validation 

Next you need to add a method that will call the ValidateAndThrow method and will process the results appropriately. Listing 10-17 shows the method, which should be added to the BusinessBase class.

Listing 10-17: The Validate Method of the BusinessBase Class

start example
 Protected Sub Validate(ByVal strProperty As String)      Try           mblnDirty = True           mobjVal.ValidateAndThrow(Me, strProperty)           mobjRules.BrokenRule(strProperty, False)      Catch exc As Exception           mobjRules.BrokenRule(strProperty, True)           Throw exc      End Try End Sub 
end example

This method is simple—it takes a property name and calls the ValidateAndThrow method. If no exceptions are thrown, the property is set to not broken; if there is an exception, the property is marked as broken and the exception is rethrown.

Next you need to modify the Region class; specifically, you need to modify the public RegionDescription property. Before you do anything else, you need to add the following Imports line to the Region.vb code module:

 Imports BusinessRules.Attributes 

Then you need to tag the RegionDescription property with your business rule attributes. Change the property signature to read as follows:

 <DisplayName("Region Description"), NotNull(), NotEmpty(), MaxLength(50)> _ Public Property RegionDescription() As String 

Note that technically you do not need the DisplayName tag here, but it cannot hurt to have it—the choice is yours. Now that you have modified the tag, you need to alter the Set part of the method to read as follows:

 Set(ByVal Value As String)      If mstrRegionDescription.Trim <> Value Then           mstrRegionDescription = Value           If Not Loading Then                Me.Validate("RegionDescription")           End If      End If End Set 

All of the functionality that had been in this method is now encapsulated in either the ValidateAndThrow method or the Validate method of the BusinessBase class. In either case, this property was originally 28 lines of code and it is now 13 lines of code—and that is just for one property!




Building Client/Server Applications with VB. NET(c) An Example-Driven Approach
Building Client/Server Applications Under VB .NET: An Example-Driven Approach
ISBN: 1590590708
EAN: 2147483647
Year: 2005
Pages: 148
Authors: Jeff Levinson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net