DEV Community

max-arshinov
max-arshinov

Posted on • Edited on

3

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!

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay