Example Page with Northwind | Download Source

Because SubSonic generates the business objects and the controllers in our data access layer, I often bind my controls to an ObjectDataSource. I love the ObjectDataSource but one of the most annoying things with it is that you must declare your select/update/delete method names as strings. The ObjectDataSource will call these methods via reflection, as the compiler doesn't have an explicit reference to the methods being invoked.  I found a solution to be able to set all the required properties of an ObjectDataSource simply by making a fake call on a lambda expression. Keep reading if you want to know how this works.  Simply set your SelectMethod like this:

ods.SetSelectMethod<ProductController>(ctrl => ctrl.FetchAll());

What are the issues with declaring names using strings?

Here is a typical ObjectDataSource declaration in your ASPX/ASCX:

<asp:ObjectDataSource ID="sqlDataSource" runat="server"
SelectMethod="FetchByProductAuthority" TypeName="Generated.StoreAuthority.ItemGroupController"
EnablePaging="True" SortParameterName="sort">
   <SelectParameters>
        <asp:Parameter Name="pa" Type="String" />
        <asp:Parameter Name="search" Type="String" />
        <asp:Parameter Name="user" Type="String" />
    </SelectParameters>
</asp:ObjectDataSource>


Why is it bad to have those properties set declaratively as strings, especially SelectMethod and TypeName?  If you ever rename your class, the select method or even add/remove its parameters, you will never know at compile time that you have broken your ODS (ObjectDataSource). Code refactoring tools don't affect these declarations either, so you are stuck using Find and Replace to change the strings in your declarations. In the worst case, you'll discover at run-time that you've broken your application.

Expressions + Lambda to the rescue

I just recently learned about Expression<T> from this article and it gave me an idea.  I managed to make something work with an extension method on the ObjectDataSource class plus a bit of extra glue logic that I will cover today.  My solution is not perfect since it’s the first time I play with Expressions, but I think it’s a good start.  I'd be ecstatic if you wanted to help me improve the syntax!

Here is how you would set your SelectMethod, TypeName and select parameters on an ODS, using my newly created extension method SetSelectMethod.

ods.SetSelectMethod<ProductController>(ctrl => ctrl.FetchAll());

 

imageWhat is happening in this piece of code?  We are calling an extension method on ObjectDataSource.  This extension method takes an Expression<Action<T>> as a parameter, where T is our controller in the model-view-controller pattern.  This basically means that we can pass an expression tree to the method that will be analyzed to figure out the information we're looking for. If you’re not familiar with what I’m saying, I recommend you take a look at this great article: Taking the magic out of Expression.  The expression tree enable us to retrieve all kinds of information from the lambda expression, like the TypeName (in this case “LavaBlast.Data.ProductController”) of the object used to make the method call and the Method name itself (SelectMethod = “FetchAll”).  We can even parse each parameter used and add them to the parameter collection of the ODS.  We can give Parameters default values derived from our expression tree of the lambda expression.  More on that later.

The figure on the left represents the expression tree for the lambda “ctrl => ctrl.FetchAll()”.

The goal of the lambda expression you pass to SetSelectMethod<T> is to make a fake call to the method you want the ODS to use when they make a Select operation.  By fake call I mean the ODS won’t invoke this lambda expression because it is there only to get analyzed for information, like the object name, method name, parameters, etc. However, since we are making a valid method call with all required parameters, you will be able to see at compile time that refactored code has affected this ODS. If we ever change this method in the future and it doesn’t compile anymore, then we’ll know it here.

It’s time for a real life example of something useful.  The last piece of code didn’t even include sorting and paging. Here is an example setting our ODS to utilize sorting and paging. Note that because paging is enabled, a SelectCountMethod is required by our ODS:

ods.SetSelectMethod<ProductController>(
    ctrl => ctrl.FetchAll(String.Empty, 0, 0),
    ctrl => ProductController.GetCount());


The method called has the following signature:

public virtual ProductCollection FetchAll(string sort, int startRowIndex, int maximumRows)


This time we have two lambda expressions, one with three empty arguments (these default values are ignored but the data types help us identify a particular method) and one defining our GetCount method.  The second one references a static method, but is very similar to what we've already covered. The properties TypeName, SelectMethod and SelectCount on the ODS will be specified by our SetSelectMethod.

Analyzing Parameters

What if we want to add parameters coming from the Session or from the value of a control on the page?  Or simply a Parameter having a default value?  The expression tree can be analyzed to discover the parameters to be added to our SelectParameter collection.

First of all, I’ll begin with a simple Parameter which has a constant default value.  Here is an example of a method filtering results depending on the logged in user's username:

ods.SetSelectMethod<ProductController>(ctrl => ctrl.FetchAll(HttpContext.Current.User.Identity))


The method declaration for this FetchAll:

public virtual C FetchAll(string user)


Imagining the currently logged in user is called "Etienne", we want to insert a Parameter with a default value of “Etienne”.  We do that by executing the value of the first parameter (HttpContext.Current.User.Identity), which will return “Etienne” and then when we create the parameter for which we set the default value.  We would create a parameter like this:

var parameter = new Parameter("user", DbType.String, "Etienne");


Now this was for a parameter with constant values, but what if the values comes from the Session (SessionParameter) or a Control value (ControlParameter)?

For example, in a page, I have a TextBox named txtSearch.  It's value can be used to filter the results in a GridView according to search terms.  We can't pass txtSearch.Text as above, as this would invoke it immediately instead of informing the ODS to query the control every time it fetches the data (it would become a Parameter instead of SessionParameter). However, we can pass a special type of parameter which will be recognized by our expression parser:

ods.SetSelectMethod<ProductController>(
    ctrl => ctrl.FetchAll(    ODSHelper.Control<string>(() => txtSearch.Text),
                            String.Empty, 0, 0),
    ctrl => ctrl.GetCount(String.Empty));


The related method declaration for FetchAll is as follow:

public virtual ProductCollection FetchAll(string search, string sort, int startRowIndex, int maximumRows)


Here the main difference  is the call to “ODSHelper.Control<string>(() => txtSearch.Text)”  as the first parameter to FetchAll.  At this point, we have to remember that this is a fake call to our method FetchAll.  The method call ODSHelper.Control<string> takes a lambda representing which control and which property on this control should be used while constructing the ControlParameter that will be added to the SelectParameter collection. From the information contained in ODSHelper.Control<string>(() => txtSearch.Text), we can construct a ControlParameter as follows and insert it into the ODS SelectParameter collection:

var parameter = new ControlParameter("search", DbType.String, "txtSearch", "Text");


The first generic parameter T we pass in the method ODSHelper.Control<T> is simply the return type.  We want our lambda to compile even if it’s a fake call, so we simply make ODSHelper.Control<T> return an object of type T, which is the data type of the parameter on our SelectMethod (in this case a string).

Analyzing a Lambda Expression

From this point forward, the article raises the geekiness bar even higher, as we drill-down into how all of this is implemented in the background. If you are interested how we analyze the expression tree, continue to read.

Let’s look at what we are doing inside the extension method SetSelectMethod<T>(Expression<Action<T>> exp).  I strongly suggest you download the project at the end of the article to see all the code file at once, but I’ll paste and explain the most important parts here.

public static void SetSelectMethod<T>(this ObjectDataSource ds, Expression<Action<T>> exp, Expression<Action<T>> expCount)


This is the declaration of our extension method.  We are extending ObjectDataSource and we need an Expression for the select method and one for the GetCount method. Since we are using an Action<T> this means the lambda expression doesn’t need to return anything.  We shall now dissect this method in greater detail.

// Make sure we have a lambda expression in the right format
var lambda = (LambdaExpression)exp;
if (lambda.Body.NodeType != ExpressionType.Call)
    throw new InvalidOperationException("Expression must be a call expression.");
 
var call = (MethodCallExpression)lambda.Body;
 
// Our lambda needs to be a call to a method
if (call.Method == null)
    throw new InvalidOperationException("Expression must be a method reference.");

 

 
Those first lines simply make sure that our lambda expression is indeed a method call on an object and that we have a valid method too.  Just from the information we have so far from the expression tree, we can already set the TypeName and SelectMethod of the ODS:

ds.TypeName = call.Object.Type.FullName;
ds.SelectMethod = call.Method.Name;

 

 
The interesting part lies in the fact that we can analyze the parameters of the method call and extract parameters the ODS needs to make the call to SelectMethod.  The loop to analyze the parameters is quite big.  I’ll put it all here and explain what is happening directly in code comments.  The most important thing to understand before reading the code is that if we encounter a method call in one of the parameters, we have to check to see if it’s one of the special method call like ODSHelper.Control or ODSHelper.Session.  We do that by checking if the method have a special attribute applied to them.  If the attribute is not there, we simply execute the method so that it returns a value and we use this value as the default value of the parameter.  Here is the code for the loop:

int i = 0;
// Then from the method call reflection information, we can get the parameters
foreach (var item in call.Method.GetParameters())
{
    // Don't add any parameters that are standard fields like sort and paging fields
    if (ds.SortParameterName != item.Name
        && ds.StartRowIndexParameterName != item.Name
        && ds.MaximumRowsParameterName != item.Name)
    {
        // Get the part of the expression tree that represents this parameter
        var mexp = call.Arguments[i];
 
        DbType type = GetDbType(item);
 
        object val = String.Empty;
        
        Parameter param = null;
        Type attribute = null;
        string propertyName = String.Empty;
 
        if (mexp is ConstantExpression)
        {
            // If it's a constant expression (for example if the parameter is 0 or a string "test")
            ConstantExpression c = mexp as ConstantExpression;
            val = c.Value; // just use this value
        }
        else if (mexp is MethodCallExpression)
        {
            // The following if is a special case if we want our parameter to be a session parameter
        // We could add more special cases for ControlParameter etc.
            var m = mexp as MethodCallExpression;
            // Check to see if the method call has the Session attribute applied to it
            if (m.Method.GetCustomAttributes(false).Count<object>(y => y.GetType() == typeof(SessionAttribute)) > 0)
            {
                attribute = typeof(SessionAttribute);
                // Just get the session field name to construct the SessionParameter
                val = Execute<string>(m.Arguments[0]);
            }
            else if (m.Method.GetCustomAttributes(false).Count<object>(y => y.GetType() == typeof(ControlAttribute)) > 0)
            {
                attribute = typeof(ControlAttribute);
 
                // A method call to pass a ControlParameter looks like this:
                // ODSHelper.Control<string, string>(() => txtName.Text)
                // Argument[0] contains the lambda expression
                // We just have to get the object name and the property name
                // and we have the values required for our ControlParameter
 
                // Argument[0] contains the lambda () => txtName.Text
                // This is an UnaryExpression and we have to get the right
                // side of this expression which is txtName.Text and analyze it
                var unary = ((UnaryExpression)m.Arguments[0]).Operand;
 
                val = LambdaHelper.GetObjectID(unary);
                propertyName = LambdaHelper.GetProperty(unary);
            }
            else
            {
                // Here the expression is more complex, maybe we are calling something like a method or accessing a property
                // to get our value back.  To get the value of the parameter passed, we actually need to construct
                // a new lambda, compile it and execute it to get the value returned and passed to our method call.
                val = Execute<object>(mexp);
            }
        }
        else
            val = Execute<object>(mexp);
 
        if (val != null)
        {
            if (attribute == typeof(SessionAttribute))
                param = new SessionParameter(item.Name, type, val.ToString()); // val contains the session field name
            else if (attribute == typeof(ControlAttribute))
                param = new ControlParameter(item.Name, type, val.ToString(), propertyName); // val contains the control name
            else
                param = new Parameter(item.Name, type, val.ToString()); // val contains the value passed to the method
 
            // Here we make sure that the parameter is not already added in the collection.
            // If it's there but it doesn't have the same value, we have to remove and add it
            bool contain = ds.SelectParameters.Contains(item.Name);
            if (!contain)
                ds.SelectParameters.Add(param);
            else if (contain && !ds.SelectParameters.HasValue(item.Name, val.ToString()))
            {
                ds.SelectParameters.Remove(item.Name);
                ds.SelectParameters.Add(param);
            }
        }
    }
    i++;
}


I think the comments explain the code pretty well.  I suggest that you download the complete source code to see the code for other method calls.

SessionParameter and ControlParameter

I want to get back to the syntax I chose to specify a parameter shall be a SessionParameter, a ControlParameter or simply a plain old Parameter.  We are using two static methods from ODSHelper class:

[Session]
public static T Session<T>(string sessionFieldName)
{
    return default(T);
}
 
[Control]
public static T Control<T>(Expression<Func<object>> action)
{
    return default(T);
}

 
Basically those methods do nothing.  They only return a default value of type T.  Why are we doing that?  We need to satisfy the parameter type of the method call where they will be used.

Why is it important to return T and not simply return a string?  For example, take the following method to be used as an ODS SelectMethod:

public TestCollection FetchAll(DateTime date, string search, string user, string sort, int startRowIndex, int maximumRows)

 
You want to pass this method with SetSelectMethod<T>.  The main difference is the first parameter.  It’ll take its value from the Session. Session[“FieldName”] will return a DateTime.  Here is how we could do it:

ObjectDataSource src = MainDataSource as ObjectDataSource;
 
src.SetSelectMethod<ItemGroupController>(
ctrl => ctrl.FetchAll(
ODSHelper.Session<DateTime>("FieldName"),
ODSHelper.Session<string>(SharedSessionVariables.GetSearchField(Page)),
HttpContext.Current.User.Identity.Name, 
String.Empty, 0, 0))

 
Pay attention to the first parameter.  What is important is that our method compiles and that our expression analyzer has enough information to fill the SessionParameter properties.  When we analyze the expression tree, we’ll be able to look at the parameter passed to the method call Session and see the Session field name.

The second really important characteristic is the custom attributes that both methods have ([Session] and [Control]).  Those unique attributes are used when we walk the expression tree to know what kind of parameter we need to create.  Without them, we could not distinguish between normal method calls and those that are here to give us information in the expression tree.

SessionParameter

ODSHelper.Session<string>(SharedSessionVariables.CurrentProductAuthority)

 

or

ODSHelper.Session<string>("CurrentProductAuthority")

 
If you don’t have a Session wrapper that defines the constant session field names, you can use the second option, which is more direct. When we analyze the expression tree, we simply evaluate the expression inside and create a SessionParameter with the result of the evaluation as the session field name.

ControlParameter

ODSHelper.Control<string>(() => txtName.Text)

 
This is how we pass in a control parameter.  Once again, we have a direct reference on the control and its property. If we remove the control later, our app won’t compile. txtName is this context is simply a TextBox that we have in our page.

ControlParameter needs two main things, the name of the control and the name of the property of the control.  We can get those two things by analyzing the expression tree of the passed lambda expression and add our ControlParameter in the parameter collection accordingly.

Parameter

For parameter inside our lambda expression that we pass, for example:

HttpContext.Current.User.Identity.Name

 
The expression analyzer simply executes this code and sets the returned value as the default value of the parameter.

Other Parameter Types

Right now my code is only supports those three kinds of parameters, but it would be pretty easy to add the other types of parameters (Query, Cookie, etc) with a little bit more work. Furthermore, the same logic could apply to the Update/Insert/Delete methods used by the ODS.

Conclusion

We need your opinion!  I think this project could be pretty useful.  The syntax is not very elegant, but I can't see a better way of doing this at this point.  Please download the project, play with the code, and give us your feedback! Ideally, this is something that would be built into the framework at a later date, but in the meantime we can still play around with it! This might not be the only way to solve the issues we are tackling, and if you have any other suggestions, let us know! (Example: Extending the ODS for each controller?)

Example Page with Northwind | Download Source

kick it on DotNetKicks.com