Introduction
Traditionally, you extend software by adding or modifying classes, and then, instantiate and use objects of classes. We call this class design. So, class design is an intermediate step in extending software. Would it be nice to extend object directly without adding or modifying classes? In this article, I discuss adding functions to object using extension methods WithBehaviors<T>()
and ActLikeWithBehaviors<T>()
.
Object Extension Methods
The following two extension methods can be used to add functions to object.
public static T WithBehaviors<T>(this object target, String[] arrMethods, Decoration preAspect, Decoration postAspect)
public static T ActLikeWithBehaviors<T>(this object target, String[] arrMethods, Decoration preAspect, Decoration postAspect)
WithBehaviors<T>()
is used to add functions to object whose class implements an interface T
while ActLikeWithBehaviors<T>()
is used to add functions to object whose class does not implement an interface T
. In the following sections, I discuss how they are used to extend object.
Using the Code
Let's start with a simple application that inserts a record into the [Sales].[SalesOrderHeader] and [Sales].[SalesOrderDetail] tables of the AdventureWorks database shipped with Microsoft SQL Server. Then, WithBehaviors<T>()
and ActLikeWithBehaviors<T>()
are used to extend it to ensure the two insertions are managed by a transaction. This is a re-writing of the same example used in Object Decoration With Impromptu-Interface.
First, let's define two classes Order
and OrderDetail
. Order
has one method to insert an order to [SalesOrderHeader] table while OrderDetail
has one method to insert the order details to [SalesOrderDetail] table.
public class Order
{
public SqlCommand Comm { get; set; }
public Order() { }
public Order(SqlCommand comm, SqlConnection conn)
{
Comm = comm;
Comm.Connection = conn;
}
public int InsertOrder(int CustomerID, DateTime DueDate, string AccountNumber, int ContactID, int BillToAddressID, int ShipToAddressID, int ShipMethodID, double SubTotal, double TaxAmt)
{
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()";
Comm.CommandText = sqlStr;
Comm.CommandType = CommandType.Text;
SqlParameter CustomerIDParameter = new SqlParameter("@CustomerID", SqlDbType.Int);
CustomerIDParameter.Direction = ParameterDirection.Input;
CustomerIDParameter.Value = CustomerID;
Comm.Parameters.Add(CustomerIDParameter);
SqlParameter DueDateParameter = new SqlParameter("@DueDate", SqlDbType.DateTime);
DueDateParameter.Direction = ParameterDirection.Input;
DueDateParameter.Value = DueDate;
Comm.Parameters.Add(DueDateParameter);
SqlParameter AccountNumberParameter = new SqlParameter("@AccountNumber", SqlDbType.Text);
AccountNumberParameter.Direction = ParameterDirection.Input;
AccountNumberParameter.Value = AccountNumber;
Comm.Parameters.Add(AccountNumberParameter);
SqlParameter ContactIDParameter = new SqlParameter("@ContactID", SqlDbType.Int);
ContactIDParameter.Direction = ParameterDirection.Input;
ContactIDParameter.Value = ContactID;
Comm.Parameters.Add(ContactIDParameter);
SqlParameter BillToAddressIDParameter = new SqlParameter("@BillToAddressID", SqlDbType.Int);
BillToAddressIDParameter.Direction = ParameterDirection.Input;
BillToAddressIDParameter.Value = BillToAddressID;
Comm.Parameters.Add(BillToAddressIDParameter);
SqlParameter ShipToAddressIDParameter = new SqlParameter("@ShipToAddressID", SqlDbType.Int);
ShipToAddressIDParameter.Direction = ParameterDirection.Input;
ShipToAddressIDParameter.Value = ShipToAddressID;
Comm.Parameters.Add(ShipToAddressIDParameter);
SqlParameter ShipMethodIDParameter = new SqlParameter("@ShipMethodID", SqlDbType.Int);
ShipMethodIDParameter.Direction = ParameterDirection.Input;
ShipMethodIDParameter.Value = ShipMethodID;
Comm.Parameters.Add(ShipMethodIDParameter);
SqlParameter SubTotalParameter = new SqlParameter("@SubTotal", SqlDbType.Float);
SubTotalParameter.Direction = ParameterDirection.Input;
SubTotalParameter.Value = SubTotal;
Comm.Parameters.Add(SubTotalParameter);
SqlParameter TaxAmtParameter = new SqlParameter("@TaxAmt", SqlDbType.Int);
TaxAmtParameter.Direction = ParameterDirection.Input;
TaxAmtParameter.Value = TaxAmt;
Comm.Parameters.Add(TaxAmtParameter);
SqlParameter scopeIDParameter = new SqlParameter("@scopeId", SqlDbType.Int);
scopeIDParameter.Direction = ParameterDirection.Output;
Comm.Parameters.Add(scopeIDParameter);
Comm.ExecuteNonQuery();
return (int)scopeIDParameter.Value;
}
}
public class OrderDetail
{
public SqlCommand Comm { get; set; }
public OrderDetail() { }
public OrderDetail(SqlCommand comm, SqlConnection conn)
{
Comm = comm;
Comm.Connection = conn;
}
public int InsertOrderDetail(int SalesOrderID, int OrderQty, int ProductID, int SpecialOfferID, double UnitPrice)
{
string sqlStr = @"INSERT INTO [Sales].[SalesOrderDetail]
([SalesOrderID], [OrderQty], [ProductID], [SpecialOfferID], [UnitPrice]) values
(@orderID, @OrderQty, @ProductID, @SpecialOfferID, @UnitPrice)";
Comm.CommandText = sqlStr;
Comm.CommandType = CommandType.Text;
SqlParameter orderIDParameter = new SqlParameter("@orderID", SqlDbType.Int);
orderIDParameter.Direction = ParameterDirection.Input;
orderIDParameter.Value = SalesOrderID;
Comm.Parameters.Add(orderIDParameter);
SqlParameter OrderQtyParameter = new SqlParameter("@OrderQty", SqlDbType.Int);
OrderQtyParameter.Direction = ParameterDirection.Input;
OrderQtyParameter.Value = OrderQty;
Comm.Parameters.Add(OrderQtyParameter);
SqlParameter ProductIDParameter = new SqlParameter("@ProductID", SqlDbType.Int);
ProductIDParameter.Direction = ParameterDirection.Input;
ProductIDParameter.Value = ProductID;
Comm.Parameters.Add(ProductIDParameter);
SqlParameter SpecialOfferIDParameter = new SqlParameter("@SpecialOfferID", SqlDbType.Int);
SpecialOfferIDParameter.Direction = ParameterDirection.Input;
SpecialOfferIDParameter.Value = SpecialOfferID;
Comm.Parameters.Add(SpecialOfferIDParameter);
SqlParameter UnitPriceParameter = new SqlParameter("@UnitPrice", SqlDbType.Float);
UnitPriceParameter.Direction = ParameterDirection.Input;
UnitPriceParameter.Value = UnitPrice;
Comm.Parameters.Add(UnitPriceParameter);
return Comm.ExecuteNonQuery();
}
}
The following code does the actually insertions.
class Program
{
static void Main(string[] args)
{
string connStr = "Integrated Security=true;Data Source=(local);Initial Catalog=AdventureWorks";
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
try
{
int iOrderId = new Order(new SqlCommand(), conn).InsertOrder(18759, DateTime.Now.AddDays(1), "10-4030-018759", 4189, 14024, 14024, 1, 174.20, 10);
int iStatus = new OrderDetail(new SqlCommand(), conn).InsertOrderDetail(iOrderId, 5, 708, 1, 28.84);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
conn.Close();
}
}
}
Run the above code, you will see one record is inserted into [SalesOrderHeader] and one record is inserted into [SalesOrderDetail].
Transaction Management
There is a problem with the above code. What about an exception is thrown after execution of the InsertOrder
but prior to completion of the InsertOrderDetail
? You end up with a dangling order without details! So, you need to put the two insertions in one transaction to ensure your data integrity.
Since Order
and OrderDetail
do not implement interfaces, we need to define interfaces to wrap them in. The two interfaces are defined as follows.
public interface IOrder
{
SqlCommand Comm { get; set; }
int InsertOrder(int CustomerID, DateTime DueDate, string AccountNumber, int ContactID, int BillToAddressID, int ShipToAddressID, int ShipMethodID, double SubTotal, double TaxAmt);
}
public interface IOrderDetail
{
SqlCommand Comm { get; set; }
int InsertOrderDetail(int SalesOrderID, int OrderQty, int ProductID, int SpecialOfferID, double UnitPrice);
}
Next, we need to have a function to manage transaction which is defined as follows.
public static void JoinSqlTransaction(AspectContext ctx, dynamic parameter)
{
try
{
ctx.Target.Comm.Transaction = parameter;
return;
}
catch (Exception ex)
{
}
}
Now, we are ready to use ActLikeWithBehaviors<T>()
to wrap an object in interface and put it in a transaction. The complete code is as follows.
static void Main(string[] args)
{
string connStr = "Integrated Security=true;Data Source=(local);Initial Catalog=AdventureWorks";
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
SqlTransaction transaction = conn.BeginTransaction();
try
{
int iOrderId = new Order(new SqlCommand(), conn).ActLikeWithBehaviors<IOrder>(new string[] { "InsertOrder" },
new Decoration(JoinSqlTransaction, transaction),
null
).InsertOrder(18759, DateTime.Now.AddDays(1), "10-4030-018759", 4189, 14024, 14024, 1, 174.20, 10);
int iStatus = new OrderDetail(new SqlCommand(), conn).ActLikeWithBehaviors<IOrderDetail>(new string[] { "InsertOrderDetail" },
new Decoration(JoinSqlTransaction, transaction),
null
).InsertOrderDetail(iOrderId, 5, 708, 1, 28.84);
transaction.Commit();
}
catch (Exception ex)
{
if (transaction != null)
transaction.Rollback();
}
conn.Close();
}
}
In the above code, an object of Order
is instantiated, which calls extension method ActLikeWithBehaviors<IOrder>()
. This extension method does two things. First, it wraps the object in interface IOrder
. Second, it attaches the transaction function JoinSqlTransaction
to the method InsertOrder
. Similarly, an object of OrderDetail
is created and the extension method is used to wrap the object in interface IOrderDetail
and attach JoinSqlTransaction
to the method InsertOrderDetail
.
Run the above code, you will see one record is inserted to table [SalesOrderHeader] and one record is inserted to table [SalesOrderDetail].
Uncomment the code //throw new Exception();
and run it, no record is added to the tables. The insertion operations are managed by transaction and the data integrity is ensured.
If the Order
and OrderDetail
have implemented the interfaces IOrder
and IOrderDetail
, respectively, there is no need to wrap them in the interfaces. Instead, you should replace ActLikeWithBehaviors<T>()
with WithBehaviors<T>()
in the above code. WithBehaviors<T>()
extension method adds functions to methods of object. Please refer to the code download for details.
Points of Interest
The object extension methods add functions to object without class design.
The above example shows how transaction management is added to objects. To see how logging, security checking and sorting are added to objects, click here (Note: The code example in this link is old but you should be able to rewrite it easily using the extesion methods discussed in this article.).