📜 ⬆️ ⬇️

Functional C #

C # is a multi-paradigm language. Recently, the roll has been outlined in the direction of the functional area . You can go ahead and add some more extension methods that allow you to write less code without “getting into” the F # territory.

PipeTo


While the Pipe Operator is not going to include the next release. Well, you can do with the method.

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

Imperative option

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

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

Did you notice? In the first option, I needed to return the view to the declaration of the variable and then go to Ok. With PipeTo execution-flow strictly left-to-right, top-down.

Either


In the real world, algorithms often contain branching than linear ones:

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

Looks not so good. Fix it with the Either method:

 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")); 

Add a null overload:

 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")); 

Unfortunately, type inference in C # is not perfect yet, so I had to add an explicit caste to an IActionResult .

Do


Get-methods of controllers do not have to create side effects, but sometimes "very necessary".

 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")); 

With this kind of code organization, a side effect with Do is sure to catch your eye during code review. Although in general, using Do is a very controversial idea.

ById


Do not find that repeating constantly q.Where(x => x.Id == id).SingleOrDefault() chore?

 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")); 

And if I don’t want to get the whole entity and I need a projection:

 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")); 

I think that by the current moment (IActionResult)new NotFoundResult("Not Found")) has already become familiar, and you yourself can easily write the OkOrNotFound method

Paginate


Perhaps there are no applications that work with data without paginated output.

Instead:

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

You can do this:

 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 IQueryableFilter


If you've read this far, you might like the idea of ​​arranging Where and OrderBy in LINQ expressions in another way :

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

In this case, it sometimes makes sense to use Where before calling Select , and sometimes after. Add a MaybeWhere method that can work with both IQueryableSpecification and 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; } 

And now you can write a method that takes into account different options:

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

Or using 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); 

If you think that IPaging , IQueryableSpecififcation and IQueryableOrderBy on one object is useless, then your version is:

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

As a result, we get three lines of code for a method that filters, sorts, and provides paginated output for any data sources that support LINQ.

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

Unfortunately, the method signatures in C # look monstrous because of the abundance of generics. Fortunately, in the application code method parameters can be omitted. The signatures of LINQ extensions look pretty much the same. How often do you specify the type returned from Select ? Praise the var who saved us from this torment.

Source: https://habr.com/ru/post/325308/


All Articles