DEV Community

max-arshinov
max-arshinov

Posted on • Edited on

LINQ Formatters

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>>
Enter fullscreen mode Exit fullscreen mode

and

enumerable.Select(x => x.FirstName  + " " + x.LastName).ToList();
// Func<Employee, String>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Here we override conversions from/to corresponding expressions, so that both:

Formatter<Employee> formatter =
  (Expression<Func<Employee, String>>)
  x => x.FirstName  + " " + x.LastName;
Enter fullscreen mode Exit fullscreen mode

and

queryable.Select(formatter).ToList();
Enter fullscreen mode Exit fullscreen mode

work just fine.

To use it on in-memory objects we are going to use the Format function

string formattedLastName = formatter.Format(obj);
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

We could use something like:

accounts.Select(x => employeeFormatter.Format(x.Employee));
Enter fullscreen mode Exit fullscreen mode

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()));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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>());
}
Enter fullscreen mode Exit fullscreen mode

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>());
}

Enter fullscreen mode Exit fullscreen mode

Happy coding!

Top comments (0)