Click here to Skip to main content
Click here to Skip to main content

Object Decoration With Impromptu-Interface

, 29 May 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
Discuss adding dynamic behaviors to object using Object Decoration with Impromptu-Interface

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)
{
    //Commenting out this line, the security check aspect will throw out an exception 
    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();

            //throw new Exception();

            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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Gary H Guo

United States United States
Object-oriented (OO) is about "classes" not "objects". But I truly believe that "objects" deserve more our attentions. If you agree, read more on... Dynamic Object Programming (DOP), Component-Based Object Extender (CBO Extender), AOP Container and Dynamic Decorator Pattern.
 
Mobile development is not just another type of front end. The real challenge is actually in the back end: How to present meaningful information in time to mobile users with exponentially increased data flooding around? Here is my first mobile solution: SmartBars - Barcode Reader, Price Comparison and Coupons.
 
Gary lives in southeast Michigan. My first programming language is FORTRAN. For the last a few years, I have primarily focused on .NET technologies with Mobile Development as my newest interest.

Comments and Discussions

 
QuestionVery good PinmemberMember 45654331-Mar-13 22:01 
AnswerRe: Very good PinmemberGary H Guo15-Jan-14 8:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141216.1 | Last Updated 29 May 2012
Article Copyright 2012 by Gary H Guo
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid