C# is a multi-paradigm programming language. Recently, the course has been set towards new functional constructions in C#. We can go further and add other extension methods that allow writing less code without ‘climbing’ in the F# territory.
PipeTo
As Pipe Operator is not going to be included in the upcoming next release, we can write the code using the method.
public static TResult PipeTo<TSource, TResult>( this TSource source, Func<TSource, TResult> func) => func(source);
Imperative variant
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);
As you can see, in the first variant, I needed to cast a look back at a the variable declaration and then proceed to Ok, while with PipeTo, the execution flow is running strictly left to right, top to bottom.
Either
In the real world, there are more branching algorithms, rather than the linear ones:
public IActionResult Get(int id) => query .Where(x => x.Id == id) .SingleOrDefault() .PipeTo(x => x != null ? Ok(x) : new NotFoundResult(“Not Found”));
It does not look so good anymore. Let’s fix it using 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 check 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, the type inference in C# is not perfect. That’s why I had to add an explicit cast to IActionResult.
Do
Get methods of controllers are not supposed to create side effects. However, sometimes they are really needed.
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 such code organization, it will be impossible to miss this effect with Do during code review. Though, using DO is a point at issue.
ById
Do not you think that constantly repeating q.Where(x => x.Id == id).SingleOrDefault() is very annoying?
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"));
What if I do not want to get the whole entity and I need 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 now the (IActionResult)new NotFoundResult(“Not Found”)) method is overused. So, you can easily write the OkOrNotFound method by yourself.
Paginate
Perhaps, there are no applications that work with data without paging.
Instead of:
.Skip((paging.Page - 1) * paging.Take) .Take(paging.Take);
You may write the code in the following way:
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);
IQueryableFilter
As it has been discussed, I suggested a different way to group Where and OrderBy in LINQ statements, which might be interesting for you:
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);
It makes sense to apply Where before the Select clause, and sometimes – after.
Add the MaybeWhere method, which works both with 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; }
Now, we can write a method that takes into account different variants:
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 you can use 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 do not like to use IPaging, IQueryableSpecififcation and IQueryableOrderBy simultaneously for one object, you can write the following code:
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 rows of code for the method that filters, sorts and provides paging for any data sources that support LINQ.
public IActionResult Get(MyNiceSpec spec) => query .Paged<int, MyNiceEntity, MyNiceDto>(spec) .PipeTo(Ok);
Unfortunately, signatures of methods in C# look humongous due to the load of generics. Luckily, we may omit method parameters in the application code. The same is with the signatures of LINQ extensions. How often do you specify the type that the Select clause returns? With var, we can avoid this torture.
Also Read:
Functional F# that slowly appears in C#
Tags: .net, c#, linq Last modified: September 23, 2021
Really nice functional solutions to some common problems.
These solutions though, although elegant, might confuse a junior who has to maintain your code a year from now. As someone who has been writing c# for 16 years I really had to pause to soak in what those extension methods were doing.
Sometimes a long and boring SingleOrDefault or Where(x=>x. Id ==… Is much easier to understand for a team.
That being said, I really like what you’ve done here. They will have a place in my memory for when they are needed ☺️
Totally agree about. So before using this stuff in production code we introduced it to the team and asked what they expect these extensions are doing. We decided not to use extensions which were not clear and renamed some methods to avoid any misunderestanding.
IMHO, a good documentation (naming, xml comment, etc) should help.