Download source.zip - 319.1 KB
Introduction
This article is going to be served as an introduction to Expert C# 2005 Business Object by RockFord Lhotka which was written for business application developer. The main content is devoted to business-focused objects called business objects (BO), and how to implement them in various real world scenarios. The book is based on CSLA .NET framework which is production quality framework and covers a number of software development cycles including system architectural design (physical design and logical design), coding and unit testing. The book reveals how internally each and every class works in the framework therefore it is quite a learning curve to any newcomers. In this series of article, I am going to introduce the CSLA.NET framework from a software developer's perspective. The CSLA .NET framework is open-sourced and its license quite liberal. It allows you modify it to create other commercial or business software; you just can't take the framework itself, modify it and sell it as a product.
What is a Business Object?
A BO normally represents a group of related business functions, and is an object which is instantiated based on any serializable class that implemented IBussinessObject
interface (which is shown below).
public interface IBusinessObject
{
}
As you can see any class can become a BO class. However in order to take advantage of the framework, you might want derive you BO class from more useful classes like BusinessBase<T>, BusinessListBase<T,C>, ReadOnlyBase<T>, etc, depends on what type of BO you are trying to create. BusinessBase<T> is the most basic and useful BO class you will use. Once a BO class is declared, you will need to declare the property of the object, setup business rules (which is used to validate if the BO is valid, before saving it to data storages ), create factory methods to create a new BO or retrieve an existing BO, create CRUD operations to create, retrieve, update and delete itself, and additional any helper functions that might help consumers of the BO. That's all. And the framework will take care of the rest. I will show in the following example in greater details.
The benefit of the framework
Everyone will raise the question why do I need the CSLA.NET framework which adds an extra layer of complexity to your already complex enough application. The answer is quite simple. It relieves you from tedious business requirements that every business application will encounter. Namely they are:
- Centralized Validation
- Authorization
- Transaction
- High scalability and high performance
- N-Level undo
Other benefits are:
- Unit testing
- Data binding support
Centralized Validation
It allows centralized validation of the Business Object. Each Business Object is responsible for validating its own state and its child BOs which it contains if any. For example, in ASP.NET programming, we can put validation of any user input, say address line can only contains up to 25 characters, in the browser via JavaScript and hope user do not turn off JavaScript execution. Or we can put the validation in the ASPX page code behind file to reject any invalid input. Or We can validate the input via stored procedure in the database. Or XLST file. Or XSD file. I did see validation code scattered in all above-mentioned places in a single project. Of course we can combine all these which results in duplication of code and create maintenance issues. A much clearer solution is to useBOs in the CSLA .NET framework. The BO class is in charge of validation and it maintains a list of rules that the BO will be validated against. In our ASP.NET example, the code behind file will read the business rule list and setup the Javascript validation routine via the validation control. If Javascript validation fails to execute, it is still fine because the validation control will do server-side check and finally the BO will be validated before saved to the database. Database in the CSLA.NET framework serves only as permanent data repository and it does not contain any business logic.
Authorization
You can add field/property based authorization to the BO. By default it is widely open which means the consumer of a BO will have read/write access all the properties of the BO. However in some cases, not all the fields will be available to all the consumer of the BO. Access can be granted via AuthorizationRules property. Once the AuthorizationRules is set for a property, the property is no longer available to all consumers of the BO except those authorized. Authorization is can also based on per object instance basis too.
Transaction
A BO can contain other child BOs. In the online shopping cart example, a shopping cart is a BO which contains many store items which are the child BOs. In CSLA.NET's terminology, we call the shopping cart BO an Editable Root object and each store item is an Editable Child object. Normally an Editable Child object cannot exist by itself; it has to be part of the Editable Root object. Since we know that BOs served as coherence interfaces to their consumer, the Editable Root object clearly a good candidate to support transaction to ensure that when the BO is being saved along with its child BOs, it remains in a consistent state. The benefit of this design is that the consumer of the BO is not aware of the existence of any transaction. All it knows is that the operation is either successful or completely failed in which case nothing will be saved and exception will be thrown. In CSLA.NET framework, it does not dictate how transaction code should be implemented or what type of data source it can communicate to. It is up the programmer to implement it.
High scalability and high performance
By leveraging CSLA.NET framework, your application can be deployed in a client-server environment in which case your application directly talking to the database(s) or other data source(s). Or without changing a single line of code, you can deploy your application to 3-Tier environment. In 3-Tier environment, your application talk to an application server which creates your BOs by communicating to the database(s) or other data source via connection pooling mechanism. It greatly reduces the contention on the database server. You can even cache the data that you know your BOs might use in next requests or shared between BOs to further increase the scalability of your application. In a 3-Tier deployment, although for any single request it might jump through more hoops and take more time, the increment in scalability yields higher throughput and better performance for the overall system.
N-Level undo
An EditableRoot object supports unlimited level of undo without reloading the data from the database(s). You can take a snapshot of a BO and make any changes and restore it to its original state. This function mainly facilitates UI developer when editing complex BOs . In our shopping cart example, before entering the store items object edit page, you can take a snapshot of the order BO by calling CopyState() function. The User can then modify the store item BOs and he/she can either choose to save the changes in which case AcceptChanges() will be called or cancel the changes in which case UndoChanges() will be called. This is 1-Level undo. Adding N-Level undo for any Editable Root objects is not a trivial task. Luckily the framework supports it right out of the box.
Unit testing
Unit testing is considered the best practice for software development. However it is often ignored by most implementations. If the business logic is scattered around everywhere in your code base, for example XSLT, Visual Basic, ASP, SQL stored procedure, ASP.NET and WinForm application, it is next to impossible to write unit test to ensure that your application conforms to the business requirements. Even if you can write the unit tests in above scenario, it often does not justify the effort which you have to pay upfront before writing any code. However if you build your application based on the idea of BOs, things turn out to be completed different. Since the BOs have well-defined interface and encapsulate all the information about the business logic, and its data, it is quite possible to write unit test to make sure that the BOs meet the business requirements. Some of the unit test code can be even cut and paste into your application to make unit test more attractive as part of the coding practice.
Data binding support
An Editable Root object has built-in support for data binding for Winform application. Since the BOs have validation rules on per property basis as I mentioned early on, there will be visual indication when certain field does not pass the validation. No code is required. You just need to create an ErrorProvider control and change its DataSource property to point to the BindingSource object. As shown in the following diagram. For ASP.NET application, data binding will still work with some tweaking. Please refer to Rockford Lhotka's Blog for more information. http://www.lhotka.net/weblog/ASPNET20DataBindingFrustration.aspx
Our First BO
We start with user BO first (for complete source, please refer to the zip file).
Step 1 declare the class
The declaration like the following looks quite confusing at first glance. The declaration of base class BusinessBase<T> is to allow some method calls of base class to return an object of type T. Say, you want to clone the object and retrieve the Name property, you can write the code, like str = user.Clone().Name, without type casting in code. Clone is the inherited from base class and Name is a property of the derived class.
public class User : BusinessBase<User>
{
}
Step 2 Define Properties
It is pretty straight-forward. The only thing you need to do is to call PropertyHasChanged() method to inform the base class that the value has been updated. It will trigger validation against the property only.
protected int _userID = 0;
protected string _name = string.Empty;
public int UserID
{
get
{
return _userID;
}
}
public string Name
{
get
{
return _name;
}
set
{
if (value == null) value = string.Empty;
if (!_name.Equals(value))
{
_name = value;
PropertyHasChanged("Name");
}
}
}
Step 3 Create business rules
You can use some pre-built rules like the ones below. Or you can create your own. Once the rules have been added, you can always use the property IsValid to determine if the BO is in valid state. Internally CSLA.NET framework is also using the flag to determine if the BO can be saved. In our demo program, we use this property to enable/disable the "Save" button.
protected override void AddBusinessRules()
{
ValidationRules.AddRule(CommonRules.StringRequired, "Name");
ValidationRules.AddRule(CommonRules.StringRequired, "FullName");
ValidationRules.AddRule(CommonRules.StringMaxLength, new CommonRules.MaxLengthRuleArgs("FullName", 15));
}
Step 4 Create factory method
The factory method will usually call the DataPortal.XYZ method to retrieve an object. CSLA.NET framework explicitly discourages the use of class constructor. All BOs are created via factory method. DataPortal.XYZ method provides a location independent way of creating an object. Behind the scenes, the framework might create the object on a remote server (usually the application server), serialize it, transport it, and de-serialize it on the local machine. As we know, when you de-serialize an object via MS DotNet library, it will return an object and there is no efficient way to de-serialize to an existing object. This is why class constructor is not used.
public static User GetUser(int userID )
{
return DataPortal.Fetch<User>(new Criteria(userID));
}
public static User NewUser()
{
return DataPortal.Create<User>();
}
Step 5 Create DataPortal_XYZ method
Depends on the type of BO you are creating, you can choose to implement some of DataPortal_XYZ methods. There are 5 in total and they are DataPortal_Create, DataPortal_Fetch, DataPortal_Update, DataPortal_Insert and DataPortal_Delete. As the names indicate, the DataPart_XYZ methods provide a way to create, fetch/retrieve, update, insert and delete the current object from its data source(s). All DataPortal_XYZ methods will only be called by CSLA.NET framework depends on which methods in your base class you are invoking and what the state of the BO is in. Scenario 1. You are create new BO, DataPortal_Create will be invoked. Scenario 2. You are creating new BO and call the save method, DataPortal_Insert will be invoked to save the current BO. Scenario 3. You are retrieving a BO from data source(s), DataPortal_Fetch will be invoked. After retrieving the BO, you change the property value, and save it, DataPortal_Update will be invoked. Scenario 4. If you are retrieving a BO from data source(s) , calling the delete method and saving the BO, DataPortal_Delete will be invoked. Much of the code here is boilerplate code. For all implementation of DataPortal_XYZ method, please see attached zip file. I just want to show the DataPortal_Fetch method here. One thing to note is the function parameter called criteria which usually stores the primary key(s) of the BO. In the factory methodGetUser, the parameter of DataPortal.Fetch<User>(new Criteria(userID)) is the same object (by value) as the parameter of DataPortal_Fetch method.
private void DataPortal_Fetch(Criteria criteria)
{
using (SqlConnection cn = new SqlConnection(Database.Connection))
{
cn.Open();
ExecuteFetch(cn, criteria);
}
}
protected void ExecuteFetch(SqlConnection cn, Criteria criteria)
{
using (SqlCommand cm = cn.CreateCommand())
{
cm.CommandType = CommandType.Text;
cm.CommandText = "Select UserID, Name, FullName, Description from UserTable where @UserID=UserID;";
cm.Parameters.AddWithValue("@UserID", criteria.UserID);
using (SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))
{
FetchObject(dr);
ValidationRules.CheckRules();
}
}
}
protected void FetchObject(SafeDataReader dr)
{
dr.Read();
_userID = dr.GetInt32("UserID");
_name = dr.GetString("Name").TrimEnd();
_fullName = dr.GetString("FullName").TrimEnd();
_description = dr.GetString("Description").TrimEnd();
}
UnitTest
Since the User BO has well defined interface and encapsulate all the data, it makes writing the unit test extremely simple. Here is the test for create a new user.
[TestMethod()]
public void UserNewTest()
{
User u;
u = UserGroupLibrary.User.NewUser();
}
test to insert a new user BO into data source.
[TestMethod()]
public void UserInsertTest()
{
User u;
u = GetValidNewUserObject();
u.Save();
}
Unit test business rules are also as simple as it gets.
[TestMethod()]
public void UserBussinessRuleTest()
{
User u;
u = GetValidNewUserObject();
Assert.IsTrue(u.IsValid, "Valid user BO failed to pass test!");
u = GetValidNewUserObject();
u.Name = null;
Assert.IsFalse(u.IsValid, "Business rule (Name field is required)for User BO failed to pass test!");
u = GetValidNewUserObject();
u.FullName = null;
Assert.IsFalse(u.IsValid, "Business rule (FullName field is required)for User BO failed to pass test!");
u = GetValidNewUserObject();
u.FullName = string.Empty.PadLeft(15, 'x');
Assert.IsTrue(u.IsValid, "Business rule (FullName field can not exceed 15 chars)for User BO failed to pass test!");
u.FullName = string.Empty.PadLeft(16, 'x');
Assert.IsFalse(u.IsValid, "Business rule (FullName field can not exceed 15 chars)for User BO failed to pass test!");
}
Databinding with WinForm Application
Once the user BO is created and unit tested, we can simply use it in our WinForm application. The following steps demonstrated how to create a user control to encapsulate the user BO editing screen.
step 1 create empty a user control
step 2 create a BindSource object and point to our User BO
step 3 create controls that display the property of the BO. The following shows the binding a text box control to the data BindingSource.
step 4 (optional) create ErrorProvider object and change its DataSource property to the BindSource object created in step 1. It provides visual indication of any validation error.
step 5 assign the BO at runtime. It is done in the user control's constructor which is before showing anything on the use control yet.
UserBindingSource.DataSource = user;
step 6 hook it up in the main WinForm class It is just one line of code to use the user control. Not much effort, isn't it?
new UserEdit(User.GetUser(Int16.Parse(textBox1.Text)), this);
The WinForm application which we just created has all the characteristics of CSLA.NET application. It is highly scalable, well-organized (which translates to low maintenance cost) and unit tested.
Conclusion
In this article, I demonstrated how to create a simple program using CSLA.NET framework. I wish I have the time to work on a sequel of this article which will demonstrate other useful classes in CSLA.NET and code generation technique for creating ready to use BOs. In fact, much of the code in this project is striped out of code generation program call CSLAcontrib to make it understandable to a new user. Hope you like it so far.
I would like also take this opportunity to thank Rogerio Caceres for his support in writing this article.