Функциональный 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")) уже тоже примелькалось и вы сами без труда напишете метод OkOrNotFoundPaginate
Пожалуй, не бывает приложений, работающих с данными без постраничного вывода.
Вместо:
.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. А для некоторых таких оберток генерят перегрузки операторов вроде||чтобы композиция не выглядела так адово, как огромная цепочка вызовов.
