具有可选泛型参数的扩展方法

本文关键字:扩展 方法 参数 泛型 | 更新日期: 2023-09-27 18:02:32

我需要实现一个基于扩展方法的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传递给fluent API的OrderBy方法。如果orderByExpression == null,也可以做其他事情。

当然,我想要这样的东西:

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

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

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

  1. 定义两个方法:一个带此参数,另一个带w/o,并从第二个方法中调用第一个方法。我不喜欢它,因为实际上API中有很多这样的方法,我必须为其中的每一个定义一个额外的方法。

  2. 使用Expression<Func<TEntity, object>>而不是Expression<Func<TEntity, TResult>>(目前是这样(。我去掉了泛型类型,但像int这样的简单(值(类型存在问题:LINQ在尝试将System.Int32强制转换为System.Object.时引发异常

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

有其他想法吗?

具有可选泛型参数的扩展方法

从调用者的角度来看,选项(1(是最好的。请记住,API的主要目标是让调用方的生活更轻松,因此在实现方面付出额外的努力应该是值得的。

选项(3(不好。您不希望进入dynamic类型引入的并发症。EF不喜欢动态表达式。

选项(2(实际上并没有那么糟糕。因此,如果它是你目前使用的,你可以保留它。让EF高兴的是,通过删除为值类型属性引入的Convert来转换传递的表达式。为此,您可以使用以下辅助方法:

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

您指定的第一个选项是显而易见的、最干净的,尽管这是最需要维护的方法。

此外,您还可以在流利的语法中引入另一个步骤。类似定义:

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方法的事实执行一些逻辑。