【问题标题】:WebAPI OData pre filtering expand queriesWebAPI OData 预过滤扩展查询
【发布时间】:2015-10-14 13:10:07
【问题描述】:

我想知道是否可以在扩展子句中的项目的 WebAPI 中预过滤 OData 结果。我只希望它根据带有已删除标志的预定义接口进行过滤。

public interface IDbDeletedDateTime
{
    DateTime? DeletedDateTime { get; set; }
}

public static class IDbDeletedDateTimeExtensions
{
    public static IQueryable<T> FilterDeleted<T>(this IQueryable<T> self) 
        where T : IDbDeletedDateTime
    {
        return self.Where(s => s.DeletedDateTime == null);
    }
}

public class Person : IDbDeletedDateTime
{
     [Key]
     public int PersonId { get; set }
     public DateTime? DeletedDateTime { get; set; }
     public virtual ICollection<Pet> Pets { get; set; }
}

public class Pet : IDbDeletedDateTime
{
     [Key]
     public int PetId { get; set }
     public int PersonId { get; set }
     public DateTime? DeletedDateTime { get; set; }
}


public class PersonController : ApiController
{
    private PersonEntities db = new PersonEntities();

    [EnableQuery]
    // GET: api/Persons
    public IQueryable<Person> GetPersons()
    {
        return db.Persons.FilterDeleted();
    }
}

你可以看到我很容易过滤被删除的人。当有人从 /api/Persons?$expand=Pets

之类的查询中删除 Pets 时,就会出现问题

有没有办法检查“宠物”的这种扩展是否是 IDbDeletedDateTime 并相应地过滤它们?也许有更好的方法来解决这个问题?

编辑:

我试图根据this answer 中的内容来解决这个问题。我不认为它可以做到,至少不是在所有情况下都可以。 ExpandedNavigationSelectItem 中唯一看起来与过滤器相关的部分是FilterClause。当它没有过滤器时,它可以为 null,它只是一个 getter 属性,这意味着如果我们愿意,我们不能用新的过滤器设置它。天气与否是否可以修改当前过滤器仅涵盖一个小用例,如果我不能重新添加过滤器,我并不特别感兴趣。

我有一个扩展方法,它将递归遍历所有扩展子句,您至少可以看到每个扩展的 FilterOption 是什么。如果有人能完全实现这 90% 的代码,那就太棒了,但我并没有屏住呼吸。

public static void FilterDeletables(this ODataQueryOptions queryOptions)
{
    //Define a recursive function here.
    //I chose to do it this way as I didn't want a utility method for this functionality. Break it out at your discretion.
    Action<SelectExpandClause> filterDeletablesRecursive = null;
    filterDeletablesRecursive = (selectExpandClause) =>
    {
        //No clause? Skip.
        if (selectExpandClause == null)
        {
            return;
        }

        foreach (var selectedItem in selectExpandClause.SelectedItems)
        {
            //We're only looking for the expanded navigation items. 
            var expandItem = (selectedItem as ExpandedNavigationSelectItem);
            if (expandItem != null)
            {
                //https://msdn.microsoft.com/en-us/library/microsoft.data.odata.query.semanticast.expandednavigationselectitem.pathtonavigationproperty(v=vs.113).aspx
                //The documentation states: "Gets the Path for this expand level. This path includes zero or more type segments followed by exactly one Navigation Property."
                //Assuming the documentation is correct, we can assume there will always be one NavigationPropertySegment at the end that we can use. 
                var edmType = expandItem.PathToNavigationProperty.OfType<NavigationPropertySegment>().Last().EdmType;
                string stringType = null;

                IEdmCollectionType edmCollectionType = edmType as IEdmCollectionType;
                if (edmCollectionType != null)
                {
                    stringType = edmCollectionType.ElementType.Definition.FullTypeName();
                }
                else
                {
                    IEdmEntityType edmEntityType = edmType as IEdmEntityType;
                    if (edmEntityType != null)
                    {
                        stringType = edmEntityType.FullTypeName();
                    }
                }

                if (!String.IsNullOrEmpty(stringType))
                {
                    Type actualType = typeof(PetStoreEntities).Assembly.GetType(stringType);
                    if (actualType != null && typeof (IDbDeletable).IsAssignableFrom(actualType))
                    {
                        var filter = expandItem.FilterOption;
                        //expandItem.FilterOption = new FilterClause(new BinaryOperatorNode(BinaryOperatorKind.Equal, new , ));
                    }
                }

                filterDeletablesRecursive(expandItem.SelectAndExpand);
            }
        }
    };

    filterDeletablesRecursive(queryOptions.SelectExpand?.SelectExpandClause);
}

【问题讨论】:

    标签: c# asp.net-web-api odata


    【解决方案1】:

    如果我理解错误,请纠正我:如果实体实现了接口IDbDeletedDateTime,您希望始终过滤实体,因此当用户想要展开导航属性时,您还想过滤该导航属性是否实现接口,对吧?

    在您当前的代码中,您使用 [EnableQuery] 属性启用了 OData 查询选项,因此 OData 将为您处理扩展查询选项,并且不会按照您想要的方式过滤 Pets。

    您可以选择实现自己的 [MyEnableQuery] 属性,并覆盖 ApplyQuery 方法:检查用户是否设置了 $expand 查询选项,如果是,检查是否请求的实体实现IDbDeletedDateTime 并进行相应的过滤。

    您可以检查here [EnableQuery] 属性的代码,并看到在ApplyQuery 方法中您可以访问对象ODataQueryOptions,该对象将包含用户设置的所有查询选项(WebApi 填充此URI 查询字符串中的对象)。

    这将是一个通用的解决方案,如果您将拥有多个具有该接口和自定义过滤的实体,您可以在所有控制器方法中使用它。如果您只希望单个控制器方法使用此功能,您还可以删除[EnableQuery] 属性,并直接在控制器方法中调用查询选项:将ODataQueryOptions 参数添加到您的方法中并手动处理查询选项。

    应该是这样的:

    // GET: api/Persons
    public IQueryable<Person> GetPersons(ODataQueryOptions queryOptions)
    {
        // Inspect queryOptions and apply the query options as you want
        // ...
        return db.Persons.FilterDeleted();
    }
    

    请参阅Invoking Query Options directly 部分以了解更多如何使用该对象。如果您阅读整篇文章,请注意 [Queryable] 属性是您的 [EnableQuery] 属性,因为该文章来自较低版本的 OData。

    希望它为您指明正确的方向以实现您想要的;)。


    编辑:关于 $expand 子句中嵌套过滤的一些信息:

    OData V4 支持过滤扩展内容。这意味着您可以在 expand 子句中嵌套一个文件管理器,例如: 获取 api/user()?$expand=followers($top=2;$select=gender)。 在这种情况下,您可以再次选择让 OData 处理它,或者自己处理它探索 ODataQueryOptions 参数: 在您的控制器中,您可以使用以下代码检查扩展选项以及它们是否具有嵌套过滤器:

    if (queryOptions.SelectExpand != null) {
        foreach (SelectItem item in queryOptions.SelectExpand.SelectExpandClause.SelectedItems) {
            if (item.GetType() == typeof(ExpandedNavigationSelectItem)) {
                ExpandedNavigationSelectItem navigationProperty =  (ExpandedNavigationSelectItem)item;
    
                // Get the name of the property expanded (this way you can control which navigation property you are about to expand)
                var propertyName = (navigationProperty.PathToNavigationProperty.FirstSegment as NavigationPropertySegment).NavigationProperty.Name.ToLowerInvariant();
    
                // Get skip and top nested filters:
                var skip = navigationProperty.SkipOption;
                var top = navigationProperty.TopOption;
    
                /* Here you should retrieve from your DB the entities that you
                   will return as a result of the requested expand clause with nested filters
                   ... */
                }
            }
        }
    

    【讨论】:

    • 我还不确定这是否能把我带到我需要去的地方,但这是一个很好的答案。太感谢了!我今天可能会试一试
    • 看起来 OData 甚至不支持在其当前实现中过滤扩展内容。不幸的是,我认为除了自己在功能中编写之外,没有其他方法可以解决这个问题。
    • OData v4 支持在 $expand 子句中过滤。我编辑了我的答案以解释如何手动处理这些嵌套过滤器(我不确定您是否可以嵌套 OData 支持的所有可能的过滤器,但至少可以嵌套 $skip、$top 用于分页、$filter 和 $select 等基本过滤器嵌套)。希望它对你有用。 (无论如何,OData 每天都在发展,所以我想这很快就会改变,并在不久的将来包含更多选项)。
    • @elbectia 查看我的编辑。我非常感谢您的帮助,因为它让我尽可能接近解决这个问题,但我认为不幸的是,这是一个无法解决的问题,至少在 OData 目前的状态下是这样。
    • @elbectia 我在询问 OData 团队目前是否可行,如果不可行,何时可行。如果我收到回复,我会再进行一次编辑。
    【解决方案2】:

    Zachary,我有一个类似的需求,我能够通过编写一个算法来解决它,该算法根据我的模型的属性向请求 ODataUri 添加额外的过滤。它检查根级实体的任何属性以及任何扩展实体的属性,以确定要添加到 OData 查询的其他过滤器表达式。

    OData v4 支持在 $expand 子句中进行过滤,但扩展实体中的 filterOption 是只读的,因此您无法修改扩展实体的过滤器表达式。您只能检查展开实体处的 filterOption 内容。

    我的解决方案是检查所有实体(根实体和扩展实体)的属性,然后在请求 ODataUri 的根过滤器中添加我需要的任何其他 $filter 选项。

    这是一个 OData 请求 URL 示例:

    /RootEntity?$expand=OtherEntity($expand=SomeOtherEntity)
    

    这是我更新后的同一个 OData 请求 URL:

    /RootEntity?$filter=OtherEntity/SomeOtherEntity/Id eq 3&$expand=OtherEntity($expand=SomeOtherEntity)
    

    完成此操作的步骤:

    1. 使用 ODataUriParser 将传入的 Url 解析为 Uri 对象

    见下文:

    var parser = new ODataUriParser(model, new Uri(serviceRootPath), requestUri);   
    var odataUri = parser.ParseUri();
    
    1. 创建一个从根向下遍历所有扩展实体的方法,并通过 ref 传递 ODataUri(以便您可以在检查每个实体时根据需要对其进行更新)

    第一种方法将检查根实体并根据根实体的属性添加任何其他过滤器。

    AddCustomFilters(ref ODataUri odataUri);
    

    AddCustomFilters 方法将遍历展开的实体并调用 AddCustomFiltersToExpandedEntity,该方法将继续向下遍历所有展开的实体以添加任何必要的过滤器。

    foreach (var item in odatauri.SelectAndExpand.SelectedItems)
    {
        AddCustomFiltersToExpandedEntity(ref ODataUri odataUri, ExpandedNavigationSelectItem expandedNavigationSelectItem, string parentNavigationNameProperty)
    }
    

    AddCustomFiltersToExpandedEntity 方法应该调用自身,因为它会循环遍历每个级别的展开实体。

    1. 在检查每个实体时更新根过滤器

    使用您的其他过滤器要求创建一个新的过滤器子句,并覆盖根级别的现有过滤器子句。 ODataUri 根级别的 $filter 有一个设置器,因此可以覆盖它。

    odataUri.Filter = new FilterClause(newFilterExpression, newFilterRange);
    

    注意:我建议使用 BinaryOperatorKind.And 创建一个新的过滤器子句,以便您的其他过滤器表达式简单地附加到 ODataUri 中已有的任何现有过滤器表达式

    var combinedFilterExpression = new BinaryOperatorNode(BinaryOperatorKind.And, odataUri.Filter.Expression, newFilterExpression);
    odataUri.Filter = new FilterClause(combinedFilterExpression, newFilterRange);
    
    1. 使用 ODataUriBuilder 根据更新后的 Uri 创建新的 Url

    见下文:

    var updatedODataUri = new Microsoft.OData.Core.UriBuilder.ODataUriBuilder(ODataUrlConventions.Default, odataUri).BuildUri();
    
    1. 将请求 Uri 替换为更新后的 Uri。

    这允许 OData 控制器使用更新的 OData Url 完成对请求的处理,其中包括您刚刚添加到根级别文件管理器的其他过滤器选项。

    ActionContext.Request.RequestUri = updatedODataUri;
    

    这应该使您能够添加所需的任何过滤选项,并 100% 确保您没有错误地更改 OData Url 结构。

    我希望这可以帮助您实现目标。

    【讨论】:

    • 这是一个很好的答案!不过,要弄清楚这些碎片应该存在的位置有点棘手。我很快就会试一试。
    • 我稍微收回了一点。我大部分都下来了。 OData uri 应该在何时何地更新?过滤器什么的?
    【解决方案3】:

    我遇到了类似的问题,我设法使用Entity Framework Dynamic Filters 解决了它。

    在您的情况下,您将创建一个过滤器来过滤掉所有已删除的记录,如下所示:

    您的 DbContext OnModelCreating 方法

    modelBuilder.Filter("NotDeleted", (Pet p) => p.Deleted, false);
    

    然后,每次查询 Pets 集合时都会应用此过滤器,无论是直接查询还是通过 OData 的 $expand 查询。您当然可以完全控制过滤器,并且可以手动或有条件地禁用它 - 动态过滤器文档中对此进行了介绍。

    【讨论】:

    • 太棒了!他们也可以在接口上工作,所以我可以做(IDbDeletedDateTime deletable) =&gt; deletable.DeletedDateTime == null。你可以让它们成为非全局的!如果它运作良好,那就太好了!谢谢。
    • 这不适用于 EF Database First,遗憾的是我现在正在使用它。 github.com/jcachat/EntityFramework.DynamicFilters/issues/12
    • @ZacharyDow 很伤心,但很高兴知道。我将此解决方案与 EF Code First 一起使用
    • 这是一次超级勇敢的尝试,我可以硬着头皮先写代码。可以肯定的是,您的答案是迄今为止最好的。
    【解决方案4】:

    我向 OData 团队询问了这个问题,我可能有一个可以使用的答案。我还不能完全测试它并使用它,但是当我能够解决它们时,它看起来会解决我的问题。我想发布这个答案以防万一这对其他人有帮助。

    话虽如此...看起来在 OData 之上有一个框架似乎处于相对起步阶段,称为 RESTier 由 Microsoft 开发。正如示例所暗示的,它似乎在 OData 之上提供了一个抽象层,允许使用这些类型的过滤器。

    这看起来像是上面的示例,其中将添加域对象中的过滤器:

    private IQueryable<Pet> OnFilterPets(IQueryable<Pet> pets)
    {
        return pets.Where(c => c.DeletedDateTime == null);
    }
    

    如果我有时间实现这个逻辑,我会回到这个答案来确认或拒绝使用这个框架。

    我从来没有能够实施这个解决方案来知道它是否值得。在我的特定用例中,有太多挑战无法证明其价值。对于新项目或真正需要这些功能的人来说,这可能是一个很好的解决方案,但我的特定用例很难将框架实现到现有逻辑中。

    您的里程可能会有所不同,这可能仍然是一个有用的检查框架。

    【讨论】:

    • 你能用这个吗?在我的场景中,当需要使用特殊过滤扩展导航属性 X 时,我继续使用我之前回答的方法:我有一个检查扩展子句的辅助方法,如果它是 X 导航属性,则返回特殊-过滤集合。当然,它不是可以以一般方式或轻松应用的东西,所以这个 RESTier 的东西听起来很有希望。
    • @elbecita 不幸的是我没有。在尝试实施时,它会自动完成很多事情,以至于我担心无法根据需要调整和调整控制器。我知道它说它有后备功能,但我并没有真正了解如何轻松使用它们。我还构建了太多东西,如果给它一个真正的尝试会严重阻碍开发进度。当时还处于起步阶段,不想赌一把。
    猜你喜欢
    • 2016-02-27
    • 1970-01-01
    • 2018-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    • 2014-12-11
    • 2020-01-20
    相关资源
    最近更新 更多