Функциональный C#
C# — язык мультипарадигмальный. В последнее время крен наметился в сторону функциональщины. Можно пойти дальше и добавить еще немного методов-расширений, позволяющих писать меньше кода, не претендуя при этом на то, чтобы превратить язык в F#.
Пока Pipe Operator не собираются включать в следующий релиз. Что-ж, можно обойтись и методом.
Императивный вариант
Заметили? В первом варианте мне нужно было вернуть взгляд к объявлению переменной и потом перейти к Ok. С PipeTo execution-flow строго слева-направо, сверху-вниз.
В реальном мире алгоритмы чаще содержат ветвления, чем бывают линейными:
Выглядит уже не так хорошо. Исправим это с помощью метода
Добавим перегрузку с проверкой на null:
К сожалению вывод типов в C# еще не идеален, поэтому пришлось добавить явный каст к
Get-методы контроллеров не должны создавать побочных эффектов, но иногда «очень надо».
Не находите, что повторять постоянно
Я думаю, что к текущему моменту
Пожалуй, не бывает приложений, работающих с данными без постраничного вывода.
Можно сделать так:
Если вы дочитали до этого места, возможно, Вам понравится идея по другому компоновать Where и OrderBy в LINQ выражениях:
При этом иногда имеет смысл применять
И теперь можно написать метод, учитывающий разные варианты:
Или с применением Queryable Extensions AutoMapper:
Если вы считаете, что лепить
В итоге получаем три строчки кода для метода, который фильтрует, сортирует и обеспечивает постраничный вывод для любых источников данных с поддержкой LINQ.
PipeTo
Пока Pipe Operator не собираются включать в следующий релиз. Что-ж, можно обойтись и методом.
public static TResult PipeTo(
this TSource source, Func 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(this TInput o, Func condition,
Func ifTrue, Func 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(this TInput o, Func ifTrue,
Func 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(this T obj, Action 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(this IQueryable queryable, TKey id)
where TEntity : class, IHasId where TKey : IComparable, IComparable, IEquatable
=> 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(this IQueryable queryable, TKey id,
Expression> projectionExpression)
where TKey : IComparable, IComparable, IEquatable
where TEntity : class, IHasId
where TProjection : class, IHasId
=> 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 : IEnumerable
{
long TotalCount { get; }
}
public static IQueryable Paginate(this IOrderedQueryable queryable, IPaging paging)
=> queryable
.Skip((paging.Page - 1) * paging.Take)
.Take(paging.Take);
public static IPagedEnumerable ToPagedEnumerable(this IOrderedQueryable queryable,
IPaging paging)
where T : class
=> From(queryable.Paginate(paging).ToArray(), queryable.Count());
public static IPagedEnumerable From(IEnumerable inner, int totalCount)
=> new PagedEnumerable(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
{
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>
public static IQueryable MaybeWhere(this IQueryable source, object spec)
where T : class
{
var specification = spec as IQueryableSpecification;
if (specification != null)
{
source = specification.Apply(source);
}
var expr = spec as Expression>;
if (expr != null)
{
source = source.Where(expr);
}
return source;
}
И теперь можно написать метод, учитывающий разные варианты:
public static IPagedEnumerable Paged(
this IQueryableProvider queryableProvider, IPaging spec ,
Expression> projectionExpression)
where TEntity : class, IHasId
where TDest : class, IHasId
=> queryableProvider
.Query()
.MaybeWhere(spec)
.Select(projectionExpression)
.MaybeWhere(spec)
.MaybeOrderBy(spec)
.OrderByIdIfNotOrdered()
.ToPagedEnumerable(spec);
Или с применением Queryable Extensions AutoMapper:
public static IPagedEnumerable Paged(this IQueryableProvider queryableProvider,
IPaging spec)
where TEntity : class, IHasId
where TDest : class, IHasId => queryableProvider
.Query()
.MaybeWhere(spec)
.ProjectTo()
.MaybeWhere(spec)
.MaybeOrderBy(spec)
.OrderByIdIfNotOrdered()
.ToPagedEnumerable(spec);
Если вы считаете, что лепить
IPaging
, IQueryableSpecififcation
и IQueryableOrderBy
на один объект богомерзко, то ваш вариант такой: public static IPagedEnumerable Paged(this IQueryableProvider queryableProvider,
IPaging paging, IQueryableOrderBy queryableOrderBy,
IQueryableSpecification entitySpec = null, IQueryableSpecification destSpec = null)
where TEntity : class, IHasId where TDest : class
=> queryableProvider
.Query()
.EitherOrSelf(entitySpec, x => x.Where(entitySpec))
.ProjectTo()
.EitherOrSelf(destSpec, x => x.Where(destSpec))
.OrderBy(queryableOrderBy)
.ToPagedEnumerable(paging);
В итоге получаем три строчки кода для метода, который фильтрует, сортирует и обеспечивает постраничный вывод для любых источников данных с поддержкой LINQ.
public IActionResult Get(MyNiceSpec spec) => query
.Paged(spec)
.PipeTo(Ok);
К сожалению сигнатуры методов в C# выглядят монструозно из-за обилия generic’ов. К счастью, в прикладном коде параметры методов можно опустить. Сигнатуры extension’ов LINQ выглядят примерно также. Как часто вы указываете возвращаемый из Select
тип? Хвала var
, который избавил нас от этого мучения.
Комментарии (1)
30 марта 2017 в 22:54
0↑
↓
В мире С++ люди еще помимо fluent interface«ов делают так: дают некоторым результатам цепочных вызовов классы-обертки, т.е. например вызовMaybe()
возвращает неT
илиnullptr
, а оберткуMaybe
. А для некоторых таких оберток генерят перегрузки операторов вроде||
чтобы композиция не выглядела так адово, как огромная цепочка вызовов.