Форматировщики LINQ

Форматирование является распространенной задачей. Обычно хочется иметь одинаковые правила форматирования во всей системе без необходимости копировать/вставлять одни и те же правила. Вот как вы можете легко сделать это в C#.

Прежде всего, правила форматирования могут применяться к объектам, расположенным в памяти, или к запросам к базе данных. В идеале инструмент должен поддерживать оба сценария. К счастью, деревья выражений могут сделать эту работу за нас. Когда вы пишете код типа

queryable.Select(x => x.FirstName  + " " + x.LastName).ToList();
// Expression<Func<Employee, String>>
Войти в полноэкранный режим Выйти из полноэкранного режима

и

enumerable.Select(x => x.FirstName  + " " + x.LastName).ToList();
// Func<Employee, String>
Ввести полноэкранный режим Выйти из полноэкранного режима

тип ламбды x => x.Name отличается, несмотря на то, что на первый взгляд они выглядят одинаково. Если хотите, ознакомьтесь с моим рассказом о деревьях выражений, чтобы лучше понять разницу.

Для нашей цели нам нужен тип Expression<Func<Employee, String>>.

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);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы переопределяем преобразования от/к соответствующим выражениям, так что и то, и другое:

Formatter<Employee> formatter =
  (Expression<Func<Employee, String>>)
  x => x.FirstName  + " " + x.LastName;
Войти в полноэкранный режим выйти из полноэкранного режима

и

queryable.Select(formatter).ToList();
Войти в полноэкранный режим Выйти из полноэкранного режима

работают просто замечательно.

Чтобы использовать его на объектах в памяти, воспользуемся функцией Format.

string formattedLastName = formatter.Format(obj);
Войти в полноэкранный режим Выйти из полноэкранного режима

Управление иерархией

Пока все хорошо. Однако что, если нужно использовать такое же форматирование для учетной записи сотрудника?

public class Account 
{
    public Employee Employee { get; set; }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы могли бы использовать что-то вроде:

accounts.Select(x => employeeFormatter.Format(x.Employee));
Войти в полноэкранный режим Выйти из полноэкранного режима

Однако этот код не будет переведен правильно, потому что он захватывает InvocationExpression, в то время как нам нужно выражение, содержащее тело сообщения формата. Короче говоря, нам нужен способ построить другое выражение из целевого выражения.

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()));
Вход в полноэкранный режим Выход из полноэкранного режима

Если вам непонятен этот абзац, обратитесь к обсуждению. Это требует некоторой работы с кодом, поэтому простого объяснения здесь нет.

Теперь с помощью метода From мы можем построить AccountFormatter, используя форматер Employee.

var employeeFormatter = (Expression<Func<Employee, String>>)
    x => x.FirstName  + " " + x.LastName;

var accountFormatter = 
    employeeFormatter.From(x => x.Employee);

accounts.Select(accountFormatter).ToList();
Вход в полноэкранный режим Выход из полноэкранного режима

Картографы данных (Automapper/Mapster/etc)

Если вы являетесь поклонником картографов данных, вы можете захотеть улучшить реализацию с помощью дополнительных методов расширения. Вот пример для 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>());
}
Войти в полноэкранный режим Выход из полноэкранного режима

Полное решение будет выглядеть следующим образом:

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

Вход в полноэкранный режим Выход из полноэкранного режима

Удачного кодирования!

Оцените статью
devanswers.ru
Добавить комментарий