LINQ Formatters
max-arshinov
Posted on August 8, 2022
Formatting is a common task. Normally one would like to have the same formatting rule across the system without needing to copy/paste the same rules. This is how you can easily do it in C#.
First of all, formatting rules can be applied to objects located in memory or to database queries. Ideally, the tool must support both scenarios. Luckily expression trees can do the job for us. When you write code like:
queryable.Select(x => x.FirstName + " " + x.LastName).ToList();
// Expression<Func<Employee, String>>
and
enumerable.Select(x => x.FirstName + " " + x.LastName).ToList();
// Func<Employee, String>
the type of x => x.Name
lambdas are different, despite they look the same at first glance. Check my talk about expression trees to better understand the difference if you like.
For our purpose, we need the Expression<Func<Employee, String>>
type.
public class Formatter<T>
{
private readonly Expression<Func<T, string>> _expression;
public Formatter(Expression<Func<T, string>> expression)
{
_expression = expression
?? throw new ArgumentNullException(nameof(expression));
}
public static implicit operator
Formatter<T>(Expression<Func<T, string>> expresssion)=>
new Formatter<T>(expresssion);
public static implicit operator
Expression<Func<T, string>>(Formatter<T> formatter) =>
formatter._expression;
public string Format(T obj) =>
(_func ?? (_func = _expression.Compile())).Invoke(obj);
}
Here we override conversions from/to corresponding expressions, so that both:
Formatter<Employee> formatter =
(Expression<Func<Employee, String>>)
x => x.FirstName + " " + x.LastName;
and
queryable.Select(formatter).ToList();
work just fine.
To use it on in-memory objects we are going to use the Format
function
string formattedLastName = formatter.Format(obj);
Managing Hierarchy
So far so good. However, what if needed to use the same formatting for the employee account?
public class Account
{
public Employee Employee { get; set; }
}
We could use something like:
accounts.Select(x => employeeFormatter.Format(x.Employee));
However, this code will not be translated properly, because it captures InvocationExpression
while what we need is an expression that contains the body of the format message. Long story short, we need a way to build another expression from the target expression.
public Formatter<TParent> From<TParent
(Expression<Func<TParent, T>> map) =>
new Formatter<TParent>(
Expression.Lambda<Func<TParent, string>>(
Expression.Invoke(_expression, map.Body),
map.Parameters.First()));
Please check the talk if you don’t understand this paragraph. This requires some code plumbing, so there is no simple explanation of this issue.
With the From
method now we can build an AccountFormatter
using the Employee formatter.
var employeeFormatter = (Expression<Func<Employee, String>>)
x => x.FirstName + " " + x.LastName;
var accountFormatter =
employeeFormatter.From(x => x.Employee);
accounts.Select(accountFormatter).ToList();
Data mappers (Automapper/Mapster/etc)
If you are a fan of data mappers, you might want to enhance the implementation with additional extension methods. Here is an example for AutoMapper:
public static class AutomapperFormatterExtensions
{
public static void FormatWith<TSource, TDest>(
this IMemberConfigurationExpression<TSource, TDest, string> mapperConfiguration,
Formatter<TSource> formatter) =>
mapperConfiguration.MapFrom<string>(formatter);
public static void FormatWith<TSource, TDest>(
this IPathConfigurationExpression<TSource, TDest, string> mapperConfiguration,
Formatter<TSource> formatter) =>
mapperConfiguration.MapFrom<string>(formatter);
public static Action<IMemberConfigurationExpression<TSource, TDest, string>> ToMapping<TSource, TDest>(
this Formatter<TSource> formatter) =>
x => x.FormatWith(formatter);
public static IMappingExpression<TSource, TDestination> ForMember<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Expression<Func<TDestination, string>> destinationMember,
Formatter<TSource> formatter) =>
mappingExpression.ForMember(
destinationMember,
formatter.ToMapping<TSource, TDestination>());
public static IMappingExpression<TSource, TDestination> ForName<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Formatter<TSource> formatter) =>
where TDestination : IHasName
mappingExpression.ForMember(
x => x.Name,
formatter.ToMapping<TSource, TDestination>());
}
The complete solution would look like:
public class Formatter<T>
{
private readonly Expression<Func<T, string>> _expression;
private Func<T, string> _func { get; set; }
public Formatter(Expression<Func<T, string>> expression)
{
_expression = expression ?? throw new ArgumentNullException(nameof(expression));
}
public Formatter<TParent> From<TParent>(Expression<Func<TParent, T>> map)
=> new Formatter<TParent>(Expression.Lambda<Func<TParent, string>>(
Expression.Invoke(_expression, map.Body), map.Parameters.First()));
public static implicit operator Formatter<T>(Expression<Func<T, string>> expresssion) =>
new Formatter<T>(expresssion);
public static implicit operator Expression<Func<T, string>>(Formatter<T> formatter) => formatter._expression;
public string Format(T obj) => (_func ?? (_func = _expression.AsFunc())).Invoke(obj);
}
public static class AutomapperFormatterExtensions
{
public static void FormatWith<TSource, TDest>(
this IMemberConfigurationExpression<TSource, TDest, string> mapperConfiguration,
Formatter<TSource> formatter) =>
mapperConfiguration.MapFrom<string>(formatter);
public static void FormatWith<TSource, TDest>(
this IPathConfigurationExpression<TSource, TDest, string> mapperConfiguration,
Formatter<TSource> formatter) =>
mapperConfiguration.MapFrom<string>(formatter);
public static Action<IMemberConfigurationExpression<TSource, TDest, string>> ToMapping<TSource, TDest>(
this Formatter<TSource> formatter) =>
x => x.FormatWith(formatter);
public static IMappingExpression<TSource, TDestination> ForMember<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Expression<Func<TDestination, string>> destinationMember,
Formatter<TSource> formatter) =>
mappingExpression.ForMember(destinationMember, formatter.ToMapping<TSource, TDestination>());
public static IMappingExpression<TSource, TDestination> ForName<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Formatter<TSource> formatter) =>
where TDestination : IHasName
mappingExpression.ForMember(x => x.Name, formatter.ToMapping<TSource, TDestination>());
}
Happy coding!
Posted on August 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.