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/
Добавить комментарий