3

I need to implement an API based on extensions methods (i.e. I have to use a static non-generic class). API should work smoothly with LINQ fluent API and mostly with IQueryable arguments. Like this:

public static class SomeExtensions
{
    public static IQueryable<TEntity> SomeMethod<TEntity>(this IQueryable<TEntity> set, ... some arguments)
    {
    }
}

Now, suppose the method should take some arguments plus an Expression<Func<TEntity, TResult>> one:

    public static IQueryable<TEntity> SomeMethod<TEntity, TResult>(
        this IQueryable<TEntity> set,
        ...,
        Expression<Func<TEntity, TResult>> orderByExpression)
    {
    }

I would like to pass the orderByExpression to OrderBy method of fluent API. Or do somethong else if orderByExpression == null.

Naturally, I'd like to have something like this:

    public static IQueryable<TEntity> SomeMethod<TEntity, TResult>(
        this IQueryable<TEntity> set,
        ...,
        Expression<Func<TEntity, TResult>> orderByExpression = null)
    {
    }

...but when calling this method w/o optional argument I have to implicitly pass generic types, because compiler doesn't know the type of TResult.

I see some possible approaches, but I don't like them much.

  1. Define two methods: one with this argument and one w/o, and call the first from the second. I don't like it because, actually, there are many such methods in the API, and I would have to define one additional method for every one of them.

  2. Use Expression<Func<TEntity, object>> instead of Expression<Func<TEntity, TResult>> (it is currently so). I got rid of generic type, but there is a problem with simple (value) types like int: LINQ raises an exception when trying to cast System.Int32 to System.Object.

  3. Maybe (haven't tried yet) I could use Expression<Func<TEntity, dynamic>> - but I don't think this is a good approach at all.

Any other ideas, anyone?

Igor Krein
  • 117
  • 2
  • 8
  • 1
    `Enumerable` and `Queryable` classes are good example of designing such API. The solution is option (2), regardless of how many overloads you need to add. – Ivan Stoev Aug 16 '16 at 13:37
  • 2
    @IvanStoev Didn't you mean option (1)? – Kapol Aug 16 '16 at 13:41
  • @Kapol Absolutely! A silly typing mistake, thank you. – Ivan Stoev Aug 16 '16 at 13:45
  • http://stackoverflow.com/questions/13717802/can-i-make-a-generic-optional-defaulting-to-a-certain-class - a similar problem – Kapol Aug 16 '16 at 13:45
  • http://stackoverflow.com/questions/18025322/generic-method-with-optional-generic-parameter – Georg Patscheider Aug 16 '16 at 13:52
  • If you have many methods to write several time, you can use T4 to generate your file. (But well, I use T4, maybe, a bit to much x) ). The only "problem" is, you will have to modify the T4 template to edit your code, instead of the code itself. It can become a bit harder to read, but you can manage easier all the parameter combinations that you want. – romain-aga Aug 16 '16 at 14:10
  • @IvanStoev I was afraid of it... – Igor Krein Aug 16 '16 at 15:01
  • @Kapol But I can't generalize a class with extension methods, so this link is not very relevant. – Igor Krein Aug 16 '16 at 15:05
  • 1
    @IgorKrein Unfortunately. But the question is though, do you really need such parameters, I mean like in the example if it's real, it basically duplicates `OrderBy` which the caller can do before calling your method w/o breaking the "fluent" syntax, e.g. `query.SomeMethod()` or `query.OrderBy(...).SomeMethod()` – Ivan Stoev Aug 16 '16 at 15:17
  • @IvanStoev The API is designed to do many things automatically, so in simple cases one method could replace all the fluent chain, i.e. both before _and_ after OrderBy. And, in very special cases, it could replace just parts of the chain. So, yes, it would be possible to combine my methods with the standard ones, like you propose. But I'd like to give the user an option to provide a special order, because I suspect that such an option would cover many special cases, and the resulting code would be much more simple. – Igor Krein Aug 16 '16 at 15:56
  • @IvanStoev You gave me an idea, though. I could call OrderBy method _first_ (in such special cases) and then, when constructing a query, check if I am dealing with ISortedQueryable. Hmm... – Igor Krein Aug 16 '16 at 16:04
  • Another option I can think of is to have something like this `public static IQueryable SomeMethod(this IQueryable set, Func, IQueryable> applyOrder = null)` so you can call `applyOrder` when specified between pre and post processing performed by the method. And the client can call it like `set.SomeMethod(q => q.OrderBy(...).ThenBy(...))`. Just a thought. In general chaining many small methods is better then calling a single method, but that's your decision. – Ivan Stoev Aug 16 '16 at 16:19

2 Answers2

2

Option (1) is the best from the caller perspective. Remember the main goal for the API is to make the caller's life easier, so putting additional efforts on the implementation side should be worth enough.

Option (3) is not good. You don't want to enter complications introduced by dynamic types. And EF does not like dynamic expressions.

Option (2) actually is not so bad. So if it's what you use currently, you can stay on it. All you need to make EF happy is to convert the passed expression by removing the Convert introduced for value type properties. To do so, you can use the following helper method:

internal static IQueryable<T> ApplyOrderBy<T>(
    this IQueryable<T> source,
    Expression<Func<T, object>> orderByExpression = null)
{
    if (orderByExpression == null) return source;
    var body = orderByExpression.Body;
    // Strip the Convert if any
    if (body.NodeType == ExpressionType.Convert)
        body = ((UnaryExpression)body).Operand;
    // Create new selector
    var keySelector = Expression.Lambda(body, orderByExpression.Parameters[0]);
    // Here we cannot use the typed Queryable.OrderBy method because
    // we don't know the TKey, so we compose a method call instead
    var queryExpression = Expression.Call(
        typeof(Queryable), "OrderBy", new[] { typeof(T), body.Type },
        source.Expression, Expression.Quote(keySelector));
    return source.Provider.CreateQuery<T>(queryExpression);
}

Here is a small test showing how the above works for different property types:

var input = new[]
{
    new { Id = 2, Name = "B", ParentId = (int?)1 },
    new { Id = 1, Name = "A", ParentId = (int?)null },
}.AsQueryable();

var output1 = input.ApplyOrderBy(e => e.Id).ToList();
var output2 = input.ApplyOrderBy(e => e.Name).ToList();
var output3 = input.ApplyOrderBy(e => e.ParentId).ToList();

Sample usage with your example:

public static IQueryable<TEntity> SomeMethod<TEntity>(
    this IQueryable<TEntity> source,
    ...,
    Expression<Func<TEntity, object>> orderByExpression = null)
{
    var result = source;
    result = preprocess(result);
    result = result.ApplyOrderBy(orderByExpression);
    result = postprocess(result);
    return result;    
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
0

The first option you specify is the obvious and the cleanest, though most maintenance-heavy way to do this.

Additionally, you could introduce another step in your fluent syntax. Like defining:

public interface ISortableQueryable<T> : IQueryable<T>
{
    IQueryable<T> WithSorting<TResult>(Expression<Func<TEntity, TResult>> orderByExpression);
}

returning it:

public static ISortableQueryable<TEntity> SomeMethod<TEntity>(
    this IQueryable<TEntity> @this, ...)
    { ... }

and providing implementation of this interface where regular IQueryable calls either redirect to the IQueryable instance it receives in constructor, or some logic is performed based on the fact whether the WithSorting method was called or not.

galenus
  • 2,087
  • 16
  • 24