Introduction
What is dynamic object programming? To answer this question, we need to answer two other questions first: What is dynamic programming? And what is object programming?
In this article, I try to answer these questions, and then present some examples to demonstrate how dynamic object programming can improve software development and maintenance in .NET.
What is Dynamic Programming
The dynamic
type in C# 4 enables operations to bypass compile-time type checking and
be resolved at runtime. It is best known that the dynamic
type can be used to simplify accesses to objects coming from a dynamic language.
Since the dynamic
type is native to C#, you can take advantage of its runtime features without involving objects from a dynamic language.
Would it be nice for a C# program to have dynamic behaviors without using a dynamic language? Or put it another way, would it be nice to use C# language
to do dynamic programming? That is our focus for this article. In this article, I show you how the dynamic
type is used to do dynamic programming in C#.
What is Object Programming
In object-oriented programming, the focus is on classes. We design classes, create objects of the classes, and use the objects in our applications.
An object behaves as defined in its class, no more and no less. In object programming, however, the focus is on objects. Extra functionality can be added
to an object without changing its class, which means an object can have behaviors not defined in its class.
With object programming, a set of behaviors is defined as methods based on business and system requirements. These behaviors are, then,
attached to the object's methods as needed in an application. At runtime, the extra behaviors are performed before and/or after the invocation of the object's methods.
What is Dynamic Object Programming
With the explanations of dynamic programming and object programming, we should be able to answer what dynamic object programming is now.
Dynamic object programming is object programming that uses the dynamic
type in the definitions of extra behaviors.
With dynamic object programming, the extra behaviors bypass compile-time type checking and are resolved at runtime. It makes the definition of extra
behaviors more flexible and powerful. It also simplifies attaching behaviors to objects in an application.
Dynamic Object Programming with CBO Extender
Component-Based Object Extender (CBO Extender) is an object extensibility framework.
It works with interfaces and extends objects directly. It is a general-purpose tool for adding functionality to objects by attaching behaviors to interface methods of objects.
With the introduction of the dynamic
type in its aspect methods (see article
Dynamic Type With Component-Based Object Extender for more details), it can be used to do dynamic object programming.
In this article, I try to address some software concerns like logging, security checking, transaction management, etc., for an application using dynamic object programming
with CBO Extender. These concerns are considered as extra behaviors aside from the functionality of business objects and are defined as aspect methods with the dynamic
type.
They are, then, added to objects as needed in an application.
CBOExtender 1.2 is the latest release of CBO Extender and can be downloaded as a NuGet package. You can also download the source code, latest updates, and more examples
at http://centurytechs.com/CBOExtender.html.
To install CBOExtender to your project from Visual Studio 2010, click Tools->Library Package Manager->Manage NuGet Packages... to open the Manage NuGet Packages dialog.
Type in CBOExtender as shown.
You probably need to install NuGet for Visual Studio 2010 before you can download the package.
Using the Code
In the following sections, several aspect methods are defined. They then are used to enhance an application by attaching them to objects in the application.
Define Aspects
The aspect methods should match the signature of DecorationDelegate2
, which is defined as follows.
public delegate void DecorationDelegate2(AspectContext2 ctx, dynamic dynPara);
A few aspect methods are defined as follows.
public static void JoinSqlTransaction(AspectContext2 ctx, dynamic parameter)
{
try
{
ctx.Target.Command.Transaction = parameter;
return;
}
catch (Exception ex)
{
throw new Exception("Failed to join transaction!", ex);
}
}
public static void EnterLog(AspectContext2 ctx, dynamic parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " +
((object)ctx.Target).GetType().ToString() + "." +
method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext2 ctx, dynamic parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = ((object)ctx.Target).GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ") exited";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void SecurityCheck(AspectContext2 ctx, dynamic parameter)
{
if (parameter.IsInRole("BUILTIN\\" + "Administrators"))
return;
throw new Exception("No right to call!");
}
JoinSqlTransaction
sets the Transaction
property of the Command
of the Target
. Note that both Target
and parameter
are dynamic
types, the compiler will not resolve their types. It is your responsibility to ensure
that Target
and parameter
have proper types when you use the method in your application. Otherwise you will get a runtime exception.
EnterLog
and ExitLog
write to Console
entering log and exit log, respectively.
SecurityCheck
checks if parameter
is in Adminstrator's role. Again, it is your responsibility to ensure parameter
has the proper type when you use the method in your application since the compiler does not resolve its type during compilation.
In the following section, I will show you how these dynamic methods can be used to add logging, security checking, and transaction management capabilities to an application.
Using CreateProxy2
Before we use the above aspect methods, let's write a small application to insert a record into the [Sales].[SalesOrderHeader]
and [Sales].[SalesOrderDetail] tables of the AdventureWorks database shipped with Microsoft SQL Server. The application code is listed as follows.
static void Main(string[] args)
{
string connStr = "Integrated Security=true;" +
"Data Source=(local);Initial Catalog=AdventureWorks";
using(IDbConnection conn = new SqlConnection(connStr))
{
IDbTransaction transaction = null;
try
{
conn.Open();
IOrder o = new Order();
o.CustomerID = 18759;
o.DueDate = DateTime.Now.AddDays(1);
o.AccountNumber = "10-4030-018759";
o.ContactID = 4189;
o.BillToAddressID = 14024;
o.ShipToAddressID = 14024;
o.ShipMethodID = 1;
o.SubTotal = 174.20;
o.TaxAmt = 10;
((ISqlOperation)o).Command = new SqlCommand();
((ISqlOperation)o).Command.Connection = (SqlConnection)conn;
int iStatus;
iStatus = o.InsertOrder();
IOrderDetail od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
((ISqlOperation)od).Command = new SqlCommand();
((ISqlOperation)od).Command.Connection = (SqlConnection)conn;
iStatus = od.InsertOrderDetail();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (transaction != null)
transaction.Rollback();
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
When executing, the InsertOrder
method of the Order
component inserts a record into the table [Sales].[SalesOrderHeader]
while the InsertOrderDetail
method of the OrderDetail
component inserts a record into the table [Sales].[SalesOrderDetail].
There is an issue with the above code: the two insertions are not in a transaction. If the program fails right after the first insertion but before
the second insertion, the data for the table [Sales].[SalesOrderDetail] will get lost. You can simulate this issue by uncommenting the line //throw new Exception();
.
To fix this issue, we need to add transaction management capability to this application by putting the insertion operations in a single transaction
so that all or no operations are performed. In addition to transaction management, we also like to add security checking to the application so that
only an administrator can insert the record to the tables by running this program. Lastly, we also like the application to generate some logs when executing.
As you may already know, we use the aspects defined in the previous section to add these enhancements to the application. The ObjectProxyFactory.CreateProxy2
method is used to add the aspects to the objects in the application. The ObjectProxyFactory.CreateProxy2
method has the following signature:
public static T CreateProxy2<T>(object target, String[] arrMethods,
Decoration2 preAspect, Decoration2 postAspect)
where:
T
- interface to be returnedtarget
- original objectarrMethods
- string array of method names to be added preprocessing aspect and/or postprocessing aspectpreAspect
- decoration for preprocessing aspectpostAspect
- decoration for postprocessing aspect
Decoration2
encapsulates a DecorationDelegate2
and a dynamic
, and has a constructor with the following signature:
public Decoration2(DecorationDelegate2 aspectHandler, dynamic parameter)
aspectHandler
is the delegate of an aspect method and the parameter
object is passed into the aspect method
as its second argument when the aspect method is invoked at runtime.
Using ObjectProxyFactory.CreateProxy2
and the above aspect methods, we can enhance the application to have transaction management,
security checking, and logging capabilities. The complete application code is listed as follows:
static void Main(string[] args)
{
Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
string connStr = "Integrated Security=true;" +
"Data Source=(local);Initial Catalog=AdventureWorks";
using (IDbConnection conn = new SqlConnection(connStr))
{
IDbTransaction transaction = null;
try
{
conn.Open();
IDbTransaction transactionObj = conn.BeginTransaction();
transaction = ObjectProxyFactory.CreateProxy2<IDbTransaction>(
transactionObj,
new string[] { "Commit", "Rollback" },
null,
new Decoration2(AppConcerns.ExitLog, null)
);
IOrder o = new Order();
o.CustomerID = 18759;
o.DueDate = DateTime.Now.AddDays(1);
o.AccountNumber = "10-4030-018759";
o.ContactID = 4189;
o.BillToAddressID = 14024;
o.ShipToAddressID = 14024;
o.ShipMethodID = 1;
o.SubTotal = 174.20;
o.TaxAmt = 10;
((ISqlOperation)o).Command = new SqlCommand();
((ISqlOperation)o).Command.Connection = (SqlConnection)conn;
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
int iStatus;
iStatus = o.InsertOrder();
IOrderDetail od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
((ISqlOperation)od).Command = new SqlCommand();
((ISqlOperation)od).Command.Connection = (SqlConnection)conn;
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
iStatus = od.InsertOrderDetail();
transaction.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (transaction != null)
transaction.Rollback();
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
The code:
transaction = ObjectProxyFactory.CreateProxy2<IDbTransaction>(
transactionObj,
new string[] { "Commit", "Rollback" },
null,
new Decoration2(AppConcerns.ExitLog, null)
);
attaches the exit log method to the methods Commit
and Rollback
of transactionObj
as a postprocessing aspect.
When using transaction
to access these methods, exit log is output right after the execution of the methods.
The code:
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
attaches the join transaction method to the method InsertOrder
of the object o
of the Order
component as a preprocessing aspect.
Note that the variable o
originally references to an object of the Order
component. Now, it references to a proxy of the object.
You can chain multiple aspects to an object like this. When using o
to call the method, it adds the method to the transaction first and then invokes the method itself.
The code:
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
attaches the entering log method and the exit log method to the method InsertOrder
of the proxy o
as a preprocessing aspect and a postprocessing aspect,
respectively. When using o
to call the method, it writes the entering log first, then invokes the method, and last writes the exit log.
The code:
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
attaches the security checking method to the method InsertOrder
of the proxy o
as a preprocessing aspect. When using o
to call the method,
it does security checking first, then invokes the method.
As you can see in the above code, you can use the proxy returned from ObjectProxyFactory.CreateProxy2
as a target to another ObjectProxyFactory.CreateProxy2
call to chain multiple aspects together. When the line iStatus = o.InsertOrder();
executes, if everything goes as expected, it checks security first, then writes
the entering log, and then joins the method of the original object to the transaction. At this point, it finishes all preprocessing aspects and starts to invoke the method
InsertOrder()
of the original object. After this, it writes the exit log.
The rest of code does the same thing to the object od
of the OrderDetail
component. It adds join transaction, entering log,
exit log, and security checking aspects to it. The code iStatus = od.InsertOrderDetail();
checks security, writes the entering log, joins the method
to the transaction, invokes the method, and writes the exit log.
The code transaction.Commit();
commits the transaction and writes the exit log.
The code transaction.Rollback();
rolls back the transaction and writes the exit log.
Run the application, and you will see the following screen. And a new record is inserted into the [Sales].[SalesOrderHeader] and [Sales].[SalesOrderDetail] tables, respectively.
Uncomment the line //throw new Exception();
and run it again, and you will see the following screen. And no record is inserted into
[Sales].[SalesOrderHeader] or [Sales].[SalesOrderDetail].
Comment out the line Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
and run it again, and you will see the following screen.
The security checking fails. No log is generated and no record is inserted into [Sales].[SalesOrderHeader] or [Sales].[SalesOrderDetail].
Points of Interest
Object programming unveils a programming paradigm which aims to improve software development by extending objects at runtime instead of extending classes at design time.
Combined with dynamic programming, it makes software development more flexible and adaptive to changes.
With the CBO Extender, you can do dynamic object programming by defining a set of methods and attaching them to objects as needed at runtime.
It complements object-oriented programming by avoiding changes of class, and therefore improving the flexibility of a software system and reducing
its maintenance cost. It limits programming to interfaces and interface methods.