可选泛型参数的扩展方法

3

我需要基于扩展方法(即我必须使用静态非泛型类)实现一个API。该API应与LINQ流畅API平稳地工作,并且大多数情况下应与IQueryable参数一起使用。像这样:

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

现在,假设该方法应该带有一些参数以及一个Expression<Func<TEntity, TResult>>参数:

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

我希望将orderByExpression传递给流畅API的OrderBy方法,如果orderByExpression == null,则执行其他操作。
自然而然地,我想要像这样的东西:
    public static IQueryable<TEntity> SomeMethod<TEntity, TResult>(
        this IQueryable<TEntity> set,
        ...,
        Expression<Func<TEntity, TResult>> orderByExpression = null)
    {
    }

...但是在调用此方法时,如果没有可选参数,我必须隐式传递泛型类型,因为编译器不知道TResult的类型。

我看到一些可能的方法,但我不太喜欢它们。

  1. 定义两个方法:一个带有这个参数,一个没有,并从第二个调用第一个。我不喜欢这个方法,因为实际上,在API中有很多这样的方法,我将不得不为每个方法定义一个额外的方法。

  2. 使用Expression<Func<TEntity, object>>代替Expression<Func<TEntity, TResult>>(目前是这样)。我摆脱了泛型类型,但是对于像int这样的简单(值)类型存在问题:当尝试将System.Int32转换为System.Object时,LINQ会引发异常。

  3. 也许(还没有尝试过)我可以使用Expression<Func<TEntity, dynamic>>——但我认为这不是一个好方法。

还有其他想法吗?


1
EnumerableQueryable类是设计此类API的良好示例。无论需要添加多少重载,解决方案都是选项(2)。 - Ivan Stoev
2
@IvanStoev 你是不是指的是选项(1)? - Kapol
@Kapol 绝对没错!是个愚蠢的打字错误,谢谢。 - Ivan Stoev
如果你有很多方法需要写多次,你可以使用T4来生成你的文件。(但是嗯,我用T4可能有点过头了x))。唯一的“问题”是,你将不得不修改T4模板来编辑你的代码,而不是代码本身。这可能会变得有点难以阅读,但你可以更轻松地管理所有想要的参数组合。 - romain-aga
1
@IgorKrein 很遗憾。但问题是,您真的需要这些参数吗?我的意思是,如果示例是真实的话,它基本上会复制 OrderBy 的功能,而调用方可以在调用您的方法之前使用 query.SomeMethod()query.OrderBy(...).SomeMethod() 而不会破坏“流畅”的语法。 - Ivan Stoev
显示剩余7条评论
2个回答

2
选项(1)从调用者的角度来看是最好的。请记住,API 的主要目标是使调用者的生活更加轻松,因此在实现方面投入额外的努力应该足够值得。
选项(3)不好。您不想介绍由动态类型引入的复杂性。而且 EF 不喜欢动态表达式。
选项(2)实际上并不那么糟糕。所以如果这是您当前使用的东西,您可以继续使用它。您需要做的就是通过删除为值类型属性引入的 Convert 来转换传递的表达式,以使 EF 开心。为此,您可以使用以下帮助器方法:
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);
}

这里有一个小测试,展示了如何针对不同的属性类型使用上述内容:
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();

使用您的示例进行样本用法:

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

现在,这看起来非常有趣。我一定会尝试一下,非常感谢! - Igor Krein

0

你指定的第一种选项是最明显、最干净,但也是最需要维护的方式。

此外,你还可以在流畅的语法中引入另一个步骤。例如定义:

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

返回它:

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

并提供此接口的实现,其中常规的IQueryable调用将重定向到构造函数中接收到的IQueryable实例,或者根据是否调用了WithSorting方法执行某些逻辑。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接