Introduction
Recently, in the discussions of my blog post Object Decoration is Functional Programming with jpolvora, he mentioned the impromptu-interface. That is exactly what I want for the object decoration (OD). With impromptu-interface, object decoration can be used to add dynamic behaviors to any objects.
Background
The object decoration is a concept from Component-Based Object Extender (CBO Extender) - an object extensibility framework that adds dynamic behaviors to objects at runtime. A prerequisite with CBO Extender is that it works with interface methods only. Therefore, instance methods, which are not defined in an interface, can not be directly attached dynamic behaviors.
With the impromptu-interface, any object can be wrapped with an interface. That means any objects now can have dynamic behaviors. If an object has interface methods, you can directly attach behaviors to them as needed. If an object does not have interface methods, you define an interface and wrap the object in it, then, attach behaviors to the interface methods as needed.
In this article, I first create a simple application to insert a record into the [Sales].[SalesOrderHeader] and [Sales].[SalesOrderDetail] tables of the AdventureWorks database shipped with Microsoft SQL Server. Then, impromptu-interface is used to wrap objects that only have instance (non-interface) methods. And finally, CBO Extender is used to enhance the application by adding logging, security checking and transaction capabilities as dynamic behaviors.
You can use NuGet from Visual Studio 2010 to get both impromptu-interface and CBOExtender by typing impromptu-interface and CBOExtender, respectively, in the search box of Manage NuGet Packages dialog. You can also click the links ImpromptuInterface and CBOExtender to download them.
Using The Code
First, let's define two POCO (Plain Old CLR Object) classes Order
and OrderDetail
. They are business objects corresponding to the [SalesOrderHeader] table and the [SalesOrderDetail] table in the AdventureWorks database, respectively.
public class Order
{
public int OrderID { get; set; }
public int CustomerID { get; set; }
public DateTime DueDate { get; set; }
public string AccountNumber { get; set; }
public int ContactID { get; set; }
public int BillToAddressID { get; set; }
public int ShipToAddressID { get; set; }
public int ShipMethodID { get; set; }
public double SubTotal { get; set; }
public double TaxAmt { get; set; }
private SqlCommand commd;
public SqlCommand Command
{
get { return commd;}
set { commd = value; }
}
public int InsertOrder()
{
string sqlStr = @"INSERT [Sales].[SalesOrderHeader]
([CustomerID], [DueDate], [AccountNumber], [ContactID], [BillToAddressID],
[ShipToAddressID], [ShipMethodID], [SubTotal], [TaxAmt]) values
(@CustomerID, @DueDate, @AccountNumber, @ContactID, @BillToAddressID,
@ShipToAddressID, @ShipMethodID, @SubTotal, @TaxAmt); SET @scopeId = SCOPE_IDENTITY()";
commd.CommandText = sqlStr;
commd.CommandType = CommandType.Text;
SqlParameter CustomerIDParameter = new SqlParameter("@CustomerID", SqlDbType.Int);
CustomerIDParameter.Direction = ParameterDirection.Input;
CustomerIDParameter.Value = CustomerID;
commd.Parameters.Add(CustomerIDParameter);
SqlParameter DueDateParameter = new SqlParameter("@DueDate", SqlDbType.DateTime);
DueDateParameter.Direction = ParameterDirection.Input;
DueDateParameter.Value = DueDate;
commd.Parameters.Add(DueDateParameter);
SqlParameter AccountNumberParameter = new SqlParameter("@AccountNumber", SqlDbType.Text);
AccountNumberParameter.Direction = ParameterDirection.Input;
AccountNumberParameter.Value = AccountNumber;
commd.Parameters.Add(AccountNumberParameter);
SqlParameter ContactIDParameter = new SqlParameter("@ContactID", SqlDbType.Int);
ContactIDParameter.Direction = ParameterDirection.Input;
ContactIDParameter.Value = ContactID;
commd.Parameters.Add(ContactIDParameter);
SqlParameter BillToAddressIDParameter = new SqlParameter("@BillToAddressID", SqlDbType.Int);
BillToAddressIDParameter.Direction = ParameterDirection.Input;
BillToAddressIDParameter.Value = BillToAddressID;
commd.Parameters.Add(BillToAddressIDParameter);
SqlParameter ShipToAddressIDParameter = new SqlParameter("@ShipToAddressID", SqlDbType.Int);
ShipToAddressIDParameter.Direction = ParameterDirection.Input;
ShipToAddressIDParameter.Value = ShipToAddressID;
commd.Parameters.Add(ShipToAddressIDParameter);
SqlParameter ShipMethodIDParameter = new SqlParameter("@ShipMethodID", SqlDbType.Int);
ShipMethodIDParameter.Direction = ParameterDirection.Input;
ShipMethodIDParameter.Value = ShipMethodID;
commd.Parameters.Add(ShipMethodIDParameter);
SqlParameter SubTotalParameter = new SqlParameter("@SubTotal", SqlDbType.Float);
SubTotalParameter.Direction = ParameterDirection.Input;
SubTotalParameter.Value = SubTotal;
commd.Parameters.Add(SubTotalParameter);
SqlParameter TaxAmtParameter = new SqlParameter("@TaxAmt", SqlDbType.Int);
TaxAmtParameter.Direction = ParameterDirection.Input;
TaxAmtParameter.Value = TaxAmt;
commd.Parameters.Add(TaxAmtParameter);
SqlParameter scopeIDParameter = new SqlParameter("@scopeId", SqlDbType.Int);
scopeIDParameter.Direction = ParameterDirection.Output;
commd.Parameters.Add(scopeIDParameter);
int i = commd.ExecuteNonQuery();
OrderID = (int)scopeIDParameter.Value;
return i;
}
}
public class OrderDetail
{
public int SalesOrderID { get; set; }
public int OrderQty { get; set; }
public int ProductID { get; set; }
public int SpecialOfferID { get; set; }
public double UnitPrice { get; set; }
private SqlCommand commd;
public SqlCommand Command
{
get { return commd; }
set { commd = value; }
}
public int InsertOrderDetail()
{
string sqlStr = @"INSERT INTO [Sales].[SalesOrderDetail]
([SalesOrderID], [OrderQty], [ProductID], [SpecialOfferID], [UnitPrice]) values
(@orderID, @OrderQty, @ProductID, @SpecialOfferID, @UnitPrice)";
commd.CommandText = sqlStr;
commd.CommandType = CommandType.Text;
SqlParameter orderIDParameter = new SqlParameter("@orderID", SqlDbType.Int);
orderIDParameter.Direction = ParameterDirection.Input;
orderIDParameter.Value = SalesOrderID;
commd.Parameters.Add(orderIDParameter);
SqlParameter OrderQtyParameter = new SqlParameter("@OrderQty", SqlDbType.Int);
OrderQtyParameter.Direction = ParameterDirection.Input;
OrderQtyParameter.Value = OrderQty;
commd.Parameters.Add(OrderQtyParameter);
SqlParameter ProductIDParameter = new SqlParameter("@ProductID", SqlDbType.Int);
ProductIDParameter.Direction = ParameterDirection.Input;
ProductIDParameter.Value = ProductID;
commd.Parameters.Add(ProductIDParameter);
SqlParameter SpecialOfferIDParameter = new SqlParameter("@SpecialOfferID", SqlDbType.Int);
SpecialOfferIDParameter.Direction = ParameterDirection.Input;
SpecialOfferIDParameter.Value = SpecialOfferID;
commd.Parameters.Add(SpecialOfferIDParameter);
SqlParameter UnitPriceParameter = new SqlParameter("@UnitPrice", SqlDbType.Float);
UnitPriceParameter.Direction = ParameterDirection.Input;
UnitPriceParameter.Value = UnitPrice;
commd.Parameters.Add(UnitPriceParameter);
return commd.ExecuteNonQuery();
}
}
The following code creates one Order
object and one OrderDetail
object, set their properties, and insert them into corresponding database tables.
static void Main(string[] args)
{
string connStr = "Integrated Security=true;Data Source=(local);Initial Catalog=AdventureWorks";
using(IDbConnection conn = new SqlConnection(connStr))
{
try
{
conn.Open();
var 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;
o.Command = new SqlCommand();
o.Command.Connection = (SqlConnection)conn;
int iStatus;
iStatus = o.InsertOrder();
var od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
od.Command = new SqlCommand();
od.Command.Connection = (SqlConnection)conn;
iStatus = od.InsertOrderDetail();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
Run the above code, you will see one record is inserted into [SalesOrderHeader] and the other record is inserted into [SalesOrderDetail].
The above code implements bare minimum business logic. In a real world application, it is likely you need security checking and logging capabilities. And you may also want that the two insertions are managed by transaction so that both insertions succeed or fail together.
Defining Dynamic Behaviors
With CBO Extender, the logging, security checking and transaction management capabilities are defined as functions, which are attached to objects as dynamic behaviors. These functions have the following signature.
void func(AspectContext2 ctx, dynamic parameter)
The following are the definitions of functions for transaction management, enter logging, exit logging and security checking, respectively.
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!");
}
Defining Interfaces
Prior to add the above dynamic behaviors to objects, we need to make sure the objects have interface methods. As you see, the objects of Order
and OrderDetail
do not have any interface methods. To attach dynamic behaviors to them, we need to wrap them in interfaces. The interfaces for each of objects are defined as follows.
public interface IOrder
{
int OrderID { get; set; }
int CustomerID { get; set; }
DateTime DueDate { get; set; }
string AccountNumber { get; set; }
int ContactID { get; set; }
int BillToAddressID { get; set; }
int ShipToAddressID { get; set; }
int ShipMethodID { get; set; }
double SubTotal { get; set; }
double TaxAmt { get; set; }
SqlCommand Command { get; set; }
int InsertOrder();
}
public interface IOrderDetail
{
int SalesOrderID { get; set; }
int OrderQty { get; set; }
int ProductID { get; set; }
int SpecialOfferID { get; set; }
double UnitPrice { get; set; }
SqlCommand Command { get; set; }
int InsertOrderDetail();
}
Wrapping Objects and Attaching Behaviors
The extension method ActLike<I>
of object can be used to wrap an object with an interface. For example, the object o
is wrapped in the IOrder
as follows.
var iOrder = o.ActLike<IOrder>();
Then, the iOrder
is used as an interface variable of IOrder
. We can start to attach behaviors to this interface varible using CreateProxy2<T>
of CBO Extender, which has the following signature.
static T CreateProxy2<T>(object target, string[] arrMethods, Decoration2 preAspect, Decoration2 postAspect);
For example, the EnterLog
function is attached to the iOrder
as dynamic behavior as follows.
iOrder = ObjectProxyFactory.CreateProxy2<IOrder>(
iOrder,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
null
);
The complete code for this application after interface wrapping and behavior attaching is 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)
);
var 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;
o.Command = new SqlCommand();
o.Command.Connection = (SqlConnection)conn;
var iOrder = o.ActLike<IOrder>();
iOrder = ObjectProxyFactory.CreateProxy2<IOrder>(
iOrder,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
iOrder = ObjectProxyFactory.CreateProxy2<IOrder>(
iOrder,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
iOrder = ObjectProxyFactory.CreateProxy2<IOrder>(
iOrder,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
int iStatus;
iStatus = iOrder.InsertOrder();
var od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
od.Command = new SqlCommand();
od.Command.Connection = (SqlConnection)conn;
var iOrderDetail = od.ActLike<IOrderDetail>();
iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
iOrderDetail,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
iOrderDetail,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
iOrderDetail,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
iStatus = iOrderDetail.InsertOrderDetail();
transaction.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (transaction != null)
transaction.Rollback();
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
In the above code, the transaction object transactionObj
returned from conn.BeginTransaction()
implements interface IDbTransaction
. Therefore, we can directly attach exiting log function AppConcerns.ExitLog
to its Commit
and Rollback
methods.
Since Order
doesn't implement an interface, we wrap its object o
with interface IOrder
using extension method ActLike<I>
of object. Then, the returned interface variable iOrder
is used to attach trarnsaction management, entering log, exiting log and security check behaviors. Now, when executing iStatus = iOrder.InsertOrder();
, it will check the security first, write entering log, then join the transaction, and last, write exiting log.
Similarly, since OrderDetail
doesn't implement an interface, we wrap its object od
with interface IOrderDetail
using extension method ActLike<I>
of object. Then, the returned interface variable iOrderDetail
is used to attach trarnsaction management, entering log, exiting log and security check behaviors. Now, when executing iStatus = iOrderDetail.InsertOrderDetail();
, it will check the security first, write entering log, then join the transaction, and last, write exiting log.
When running the code, you see the following screen.
Uncomment the code //throw new Exception();
and run it, you see the following screen.
Points of Interest
Application development can never be easier with impromptu-interface and object decoration. You design you business objects (classes) strictly to address business logic. You leave other concerns (security, logging, transaction or requirement changes) to the client-side. Object decoration with impromptu-interface has the following advantages.
- It is client-side programming, which means your business objects are stable.
- It is programming-to-interface, which means your system can start as loosely-coupled and stay loosely-coupled.
- It is functional programming, which means you write functions for new behaviors.
- It is dynamic programming, which means both dynamic behaviors and dynamic types.