Dynamically Generating Lambda Expressions at Runtime From Properties Obtained Through Reflection on Generic Types

Lately I’ve been having to export some of my data entities into CSV files, and I’ve been using the CSVHelper nuget package to achieve this. As is common, property names don’t translate well into readable column headers, so you have to provide some kind of property to string mapping.

This is how CSVHelper handles it:

namespace MyApplication.CSVMapping
{
	public class MyModelCsvMap : CsvClassMap
	{
		public override void CreateMap()
		{
			Map(m => m.Id).Name("Model Id");
			Map(m => m.Description).Name("Model Description");
			Map(m => m.StartDate).Name("Start Date");
			Map(m => m.EndDate).Name("End Date");
			Map(m => m.RunDate).Name("Run Date");
		}
	}
}

Nothing too fancy, just passing my model type into the derived class, and going through each class member, setting the Name property.

However, as is also common, I may also have a form tied to this model and I want to use the built in DataAnnotations to set the form labels for each field, like so:

namespace MyApplication.Models
{
	public partial class MyModel
	{
		[DisplayName("Model ID")]
		public int Id { get; set; }
		[DisplayName("Model Description")]
		public string Description { get; set; }
		[DisplayName("Start Date")]
		public DateTime StartDate { get; set; }
		[DisplayName("End Date")]
		public DateTime EndDate { get; set; }
		[DisplayName("Run Date")]
		public DateTime RunDate { get; set; }
	}
}

Noticing some redundancy here? Could I perhaps have CSVHelper get the property column header names from the DisplayName Attribute in the model rather than having to create a seperate CsvClassMap? That way I wouldn’t have to repeat my property to string mappings.

For this I will have to create a generic version of the CsvClassMap class, which takes in my entity type. From there I can get all the properties in that type, and start iterating through them. For each property, I check if it has a DisplayName attribute, and if it does, get what the value is. The tricky part is passing in the property into CSVHelper’s map method which expects a Expression<Func<TEntity, object>>. Here’s the complete code:

using System;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CsvHelper.Configuration;

namespace MyApplication.Common
{
	public class BaseCsvMap : CsvClassMap where TEntity : class
	{
		public override void CreateMap()
		{
			PropertyInfo[] props = typeof(TEntity).GetProperties();
			foreach (PropertyInfo prop in props)
			{
				var displayAttribute = prop.GetCustomAttributes(false).FirstOrDefault(a => a.GetType() == typeof(DisplayNameAttribute)) as DisplayNameAttribute;
				if (displayAttribute != null)
				{
					var parameterExpression = Expression.Parameter(typeof(TEntity), "x");
					var memberExpression = Expression.PropertyOrField(parameterExpression, prop.Name);
					var memberExpressionConversion = Expression.Convert(memberExpression, typeof(object));
					var lambda = Expression.Lambda<Func<TEntity, object>>(memberExpressionConversion, parameterExpression);
					Map(lambda).Name(displayAttribute.DisplayName);
				}
			}
		}
	}
}

That should be fairly self explanatory. The only strange “gotcha” is having to call Expression.Convert() before constructing the lambda expression. This is because the expression explicitly expects “object” as it’s type, and your entity likely contains typed members ie. strings, ints, decimals, etc.

You can also modify the above class to working with any custom attributes that you may have defined, just remember to pass true into the GetCustomAttributes() method.

4 Comments

  1. Bryan · February 25, 2014 Reply

    Thanks for this awesome example. Works perfect. This is exactly what I have been looking for!

  2. Giovanni · June 22, 2015 Reply

    Just what I was looking for. Thanks!

  3. Jennifer G · March 9, 2017 Reply

    Just what I needed! Thank you!

  4. Johann · May 4, 2018 Reply

    Excellent post. Brilliant. Thank you very much. It was exactly what i was looking for.

Leave a Reply to Johann