Функциональный C#

от автора

C# — язык мультипарадигмальный. В последнее время крен наметился в сторону функциональщины. Можно пойти дальше и добавить еще немного методов-расширений, позволяющих писать меньше кода, не претендуя при этом на то, чтобы превратить язык в F#.

PipeTo

Пока Pipe Operator не собираются включать в следующий релиз. Что-ж, можно обойтись и методом.

public static TResult PipeTo<TSource, TResult>(     this TSource source, Func<TSource, TResult> func)     => func(source);

Императивный вариант

public IActionResult Get() {     var someData = query         .Where(x => x.IsActive)         .OrderBy(x => x.Id)         .ToArray();     return Ok(someData); } 

С PipeTo

public IActionResult Get() =>  query     .Where(x => x.IsActive)     .OrderBy(x => x.Id)     .ToArray()     .PipeTo(Ok);

Заметили? В первом варианте мне нужно было вернуть взгляд к объявлению переменной и потом перейти к Ok. С PipeTo execution-flow строго слева-направо, сверху-вниз.

Either

В реальном мире алгоритмы чаще содержат ветвления, чем бывают линейными:

public IActionResult Get(int id) =>  query     .Where(x => x.Id == id)     .SingleOrDefault()     .PipeTo(x => x != null ? Ok(x) : new NotFoundResult(“Not Found”));

Выглядит уже не так хорошо. Исправим это с помощью метода Either:

public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, bool> condition,     Func<TInput, TOutput> ifTrue, Func<TInput, TOutput> ifFalse)     => condition(o) ? ifTrue(o) : ifFalse(o);  public IActionResult Get(int id) =>  query     .Where(x => x.Id == id)     .SingleOrDefault()     .Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

Добавим перегрузку с проверкой на null:

public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, TOutput> ifTrue,     Func<TInput, TOutput> ifFalse)     => o.Either(x => x != null, ifTrue, ifFalse);  public IActionResult Get(int id) =>  query     .Where(x => x.Id == id)     .SingleOrDefault()     .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

К сожалению вывод типов в C# еще не идеален, поэтому пришлось добавить явный каст к IActionResult.

Do

Get-методы контроллеров не должны создавать побочных эффектов, но иногда «очень надо».

public static T Do<T>(this T obj, Action<T> action) {     if (obj != null)     {         action(obj);     }      return obj; }  public IActionResult Get(int id) =>  query     .Where(x => x.Id == id)     .Do(x => ViewBag.Title = x.Name)     .SingleOrDefault()     .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

При такой организации кода побочный эффект с Do обязательно бросится в глаза во время code review. Хотя в целом использование Do — очень спорная идея.

ById

Не находите, что повторять постоянно q.Where(x => x.Id == id).SingleOrDefault() муторно?

public static TEntity ById<TKey, TEntity>(this IQueryable<TEntity> queryable, TKey id)     where TEntity : class, IHasId<TKey> where TKey : IComparable, IComparable<TKey>, IEquatable<TKey>     => queryable.SingleOrDefault(x => x.Id.Equals(id));  public IActionResult Get(int id) =>  query     .ById(id)     .Do(x => ViewBag.Title = x.Name)     .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

А если, я не хочу получать сущность целиком и мне нужна проекция:

public static TProjection ById<TKey, TEntity, TProjection>(this IQueryable<TEntity> queryable, TKey id, Expression<Func<TEntity, TProjection>> projectionExpression)     where TKey : IComparable, IComparable<TKey>, IEquatable<TKey>     where TEntity : class, IHasId<TKey>     where TProjection : class, IHasId<TKey>     => queryable.Select(projectionExpression).SingleOrDefault(x => x.Id.Equals(id));     public IActionResult Get(int id) =>  query     .ById(id, x => new {Id = x.Id, Name = x.Name, Data = x.Data})     .Do(x => ViewBag.Title = x.Name)     .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

Я думаю, что к текущему моменту (IActionResult)new NotFoundResult("Not Found")) уже тоже примелькалось и вы сами без труда напишете метод OkOrNotFound

Paginate

Пожалуй, не бывает приложений, работающих с данными без постраничного вывода.

Вместо:

.Skip((paging.Page - 1) * paging.Take) .Take(paging.Take);

Можно сделать так:

public interface IPagedEnumerable<out T> : IEnumerable<T> {     long TotalCount { get; } }  public static IQueryable<T> Paginate<T>(this IOrderedQueryable<T> queryable, IPaging  paging)  => queryable     .Skip((paging.Page - 1) * paging.Take)     .Take(paging.Take);  public static IPagedEnumerable<T> ToPagedEnumerable<T>(this IOrderedQueryable<T> queryable,     IPaging paging)     where T : class     => From(queryable.Paginate(paging).ToArray(), queryable.Count());  public static IPagedEnumerable<T> From<T>(IEnumerable<T> inner, int totalCount)     =>  new PagedEnumerable<T>(inner, totalCount);  public IActionResult Get(IPaging paging) =>  query     .Where(x => x.IsActive)     .OrderBy(x => x.Id)     .ToPagedEnumerable(paging)     .PipeTo(Ok);

IQueryableSpecification

Если вы дочитали до этого места, возможно, Вам понравится идея по другому компоновать Where и OrderBy в LINQ выражениях:

public class MyNiceSpec : AutoSpec<MyNiceEntity> {     public int? Id { get; set; }      public string Name { get; set; }      public string Code { get; set; }      public string Description { get; set; } }  public IActionResult Get(MyNiceSpec spec) =>  query     .Where(spec)     .OrderBy(spec)     .ToPagedEnumerable(paging)     .PipeTo(Ok); 

При этом иногда имеет смысл применять Where вызова Select, а иногда — после. Добавим метод MaybeWhere, который сможет работать как с IQueryableSpecification, так и с Expression<Func<T, bool>>

public static IQueryable<T> MaybeWhere<T>(this IQueryable<T> source, object spec)     where T : class {     var specification = spec as IQueryableSpecification<T>;     if (specification != null)     {         source = specification.Apply(source);     }      var expr = spec as Expression<Func<T, bool>>;     if (expr != null)     {         source = source.Where(expr);     }      return source; }

И теперь можно написать метод, учитывающий разные варианты:

public static IPagedEnumerable<TDest> Paged<TEntity, TDest>( this IQueryableProvider queryableProvider, IPaging spec , Expression<Func<TEntity, TDest>> projectionExpression)     where TEntity : class, IHasId     where TDest : class, IHasId     => queryableProvider         .Query<TEntity>()         .MaybeWhere(spec)         .Select(projectionExpression)         .MaybeWhere(spec)         .MaybeOrderBy(spec)         .OrderByIdIfNotOrdered()         .ToPagedEnumerable(spec);

Или с применением Queryable Extensions AutoMapper:

public static IPagedEnumerable<TDest> Paged<TEntity, TDest>(this IQueryableProvider queryableProvider,     IPaging spec)     where TEntity : class, IHasId     where TDest : class, IHasId => queryableProvider         .Query<TEntity>()         .MaybeWhere(spec)         .ProjectTo<TDest>()         .MaybeWhere(spec)         .MaybeOrderBy(spec)         .OrderByIdIfNotOrdered()         .ToPagedEnumerable(spec);

Если вы считаете, что лепить IPaging, IQueryableSpecififcation и IQueryableOrderBy на один объект богомерзко, то ваш вариант такой:

public static IPagedEnumerable<TDest> Paged<TEntity, TDest>(this IQueryableProvider queryableProvider,     IPaging paging, IQueryableOrderBy<TDest> queryableOrderBy, IQueryableSpecification<TEntity> entitySpec = null, IQueryableSpecification<TDest> destSpec = null)     where TEntity : class, IHasId where TDest : class     => queryableProvider         .Query<TEntity>()         .EitherOrSelf(entitySpec, x => x.Where(entitySpec))         .ProjectTo<TDest>()         .EitherOrSelf(destSpec, x => x.Where(destSpec))         .OrderBy(queryableOrderBy)         .ToPagedEnumerable(paging);

В итоге получаем три строчки кода для метода, который фильтрует, сортирует и обеспечивает постраничный вывод для любых источников данных с поддержкой LINQ.

public IActionResult Get(MyNiceSpec spec) =>  query     .Paged<int, MyNiceEntity, MyNiceDto>(spec)     .PipeTo(Ok);

К сожалению сигнатуры методов в C# выглядят монструозно из-за обилия generic’ов. К счастью, в прикладном коде параметры методов можно опустить. Сигнатуры extension’ов LINQ выглядят примерно также. Как часто вы указываете возвращаемый из Select тип? Хвала var, который избавил нас от этого мучения.
ссылка на оригинал статьи https://habrahabr.ru/post/325308/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *