My previous posts in this series revolved around using lambdas to encapsulate logic. In this post I'll show a different side of the lambdas, the expressions.
Lambda Expressions
We have already seen that lambdas can be used to create anonymous methods to be passed wherever a delegate is expected. That's not the end of the story, though.
Lambdas can also be used where an Expression<DelegateType>
is expected. The difference is that the compiler will not resolve the lambda to an anonymous method. Instead the lambda will be converted to an expression tree, which is a way to describe the code in the lambda.
If you used LINQ to SQL for example, you have already been using lambdas that are converted to expressions. Expressions are what enable LINQ to SQL to convert a LINQ query like this:
from cust in Customers
where cust.City == "London"
&& cust.ContactTitle == "Sales Representative"
select cust
into a SQL query like this:
SELECT
[t0].[CustomerID], [t0].[CompanyName],
[t0].[ContactName], [t0].[ContactTitle],
[t0].[Address], [t0].[City], [t0].[Region],
[t0].[PostalCode], [t0].[Country],
[t0].[Phone], [t0].[Fax]
FROM [Customers] AS [t0]
WHERE
([t0].[City] = "London")
AND ([t0].[ContactTitle] = "Sales Representative")
In particular, pay attention to how the where
clause was converted from C# syntax to SQL syntax.
Yet another JSON converter
Let's illustrate how expressions can be used by creating some code to convert objects to JSON.
What I will try to produce here is a converter that let's me specify which properties of my object will be included in the JSON version of the object.
One way to accomplish this could be by simply passing a list of property names to a conversion function that would work some reflection magic and extract the properties into the JSON format.
string userJson = ConvertToJson(userObj, "Name", "Email", "Age");
The biggest problem here, in my opinion, is not the reflection activity behind the scene, it's those strings. If you change the name of any of the properties the compiler and the refactoring tools won't be able to help you updating those strings. You may end up with runtime errors.
With lambdas and expressions we can do better. Here's the syntax that I will build.
class Company
{
public string Name { get; set; }
public int CompanyID { get; set; }
public string Address { get; set; }
public bool PlatinumCustomer { get; set; }
public string InternalControlNumber { get; set; }
}
// object that we will convert
var acme = new Company {
Name = "ACME, Inc." ,
InternalControlNumber = "3X77-AC",
Address = "123 Main St, Anytown, ST",
PlatinumCustomer = true,
CompanyID = 789
};
//convert it but only copy a few properties
string json = ConvertToJson<Company>(acme,
c => c.Name,
c => c.CompanyID,
c => c.PlatinumCustomer);
The resulting JSON string, stored in the json
variable will be:
{"Name":"ACME, Inc.", "CompanyID":789, "PlatinumCustomer":true}
Each of the lambdas passed in contain just one access to an existing property. As soon as you start typing this you'll already see the advantage of working with strongly-typed constructs like this. IntelliSense will automatically list the Company
properties and methods once you type c.
. Also, if you rename one of the properties, the IDE will offer to rename all references to that property, and the lambda will also be included in the renaming. Lastly, your code won't even compile if you make a typo.
The code for ConvertToJson
will receive an array of expressions of type Expression<Func<T, object>>
. This type looks complicated but if we think about it from the inside out, Func<T, object>
is something you typically see in regular lambdas. It's a delegate type. As soon as we decorate that delegate type with an Expression< T >
around it, the compiler will continue to accept the lambda syntax for anonymous methods but now the method never gets invoked, it will become an expression tree, which is then passed to the function. Here's the function.
public static string ConvertToJson<T>(
T data,
params Expression<Func<T, object>>[] properties)
{
int exportedFields = 0;
StringBuilder json = new StringBuilder();
json.Append("{");
foreach(var prop in properties)
{
string propName = null;
object propVal = null;
if(prop.Body.NodeType == ExpressionType.MemberAccess)
{
PropertyInfo pi =
(PropertyInfo)((MemberExpression)prop.Body).Member;
propVal = pi.GetValue(data, null);
propName = pi.Name;
}
else if(prop.Body.NodeType == ExpressionType.Convert)
{
UnaryExpression expr = (UnaryExpression)prop.Body;
PropertyInfo pi =
(PropertyInfo)((MemberExpression)expr.Operand).Member;
propVal = pi.GetValue(data, null);
propName = pi.Name;
}
if(propName != null)
{
string stringVal = null;
if(propVal == null) stringVal = "null";
else if(propVal is string) stringVal = "\"" + propVal + "\"";
else if(propVal is bool ||
propVal is byte ||
propVal is int ||
propVal is long ||
propVal is short)
stringVal = propVal.ToString().ToLower();
if(exportedFields > 0) json.Append(", ");
json.AppendFormat(@"""{0}"":{1}", propName, stringVal);
exportedFields++;
}
}
json.Append("}");
return json.ToString();
}