WebAPI OData预过滤扩展查询

本文关键字:扩展 查询 过滤 OData WebAPI | 更新日期: 2023-09-27 18:17:35

我想知道是否有可能对扩展子句中的项在WebAPI中预过滤OData结果。我只希望它基于带有Deleted标志的预定义接口进行过滤。

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的查询中删除宠物?扩大美元=宠物

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

编辑:

我试着根据这个答案中的内容来解决这个问题。我不认为这是可以做到的,至少不是在所有情况下。ExpandedNavigationSelectItem中唯一看起来与滤波器有关的部分是FilterClause。当它没有过滤器并且它只是一个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);
}

WebAPI OData预过滤扩展查询

如果我理解错了,请纠正我:如果它们实现了接口IDbDeletedDateTime,你想要始终过滤实体,所以当用户想要扩展导航属性时,你也想要过滤该导航属性是否实现了接口,对吧?

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

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

您可以在这里检查[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();
}

请参阅直接调用查询选项一节,以了解如何使用该对象。如果您阅读了整篇文章,请注意[Queryable]属性就是您的[EnableQuery]属性,因为本文来自较低版本的OData。

希望它能指引你朝着正确的方向去实现你想要的。


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

OData V4支持在扩展内容中进行过滤。这意味着你可以在展开子句中嵌套一个过滤器,像这样:得到的api/用户()? $扩大=追随者(高层= 2;选择美元=性别)。在这种情况下,你可以选择让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
               ... */
            }
        }
    }

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();
  • 创建一个方法,该方法将从根向下遍历到所有扩展的实体,并通过ref传递ODataUri(以便您可以在检查每个实体时根据需要更新它)
  • 第一个方法将检查根实体,并根据根实体的属性添加任何额外的过滤器。

    AddCustomFilters(ref ODataUri odataUri);
    
    AddCustomFilters方法将遍历扩展的实体并调用AddCustomFiltersToExpandedEntity,该方法将继续遍历所有扩展的实体以添加任何必要的过滤器。

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

    方法AddCustomFiltersToExpandedEntity应该在每个级别的扩展实体上循环时调用自己。

  • 在检查每个实体时更新根过滤器
  • 用额外的过滤器需求创建一个新的过滤器子句,并在根级别覆盖现有的过滤器子句。ODataUri根级的$filter有一个setter,所以它可以被覆盖。

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

    注意:我建议使用BinaryOperatorKind创建一个新的过滤器子句。和,以便将额外的筛选表达式简单地附加到ODataUri

    中已经存在的任何筛选表达式中。
    var combinedFilterExpression = new BinaryOperatorNode(BinaryOperatorKind.And, odataUri.Filter.Expression, newFilterExpression);
    odataUri.Filter = new FilterClause(combinedFilterExpression, newFilterRange);
    
  • 使用ODataUriBuilder基于更新后的Uri创建一个新的Url
  • 见下文:

    var updatedODataUri = new Microsoft.OData.Core.UriBuilder.ODataUriBuilder(ODataUrlConventions.Default, odataUri).BuildUri();
    
  • 用更新后的Uri替换请求Uri
  • 这允许OData控制器使用更新后的OData Url完成对请求的处理,该Url包含了您刚刚添加到根级过滤器的附加过滤器选项。

    ActionContext.Request.RequestUri = updatedODataUri;
    

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

    我希望这能帮助你实现你的目标。

    我有一个类似的问题,我设法解决它使用实体框架动态过滤器

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

    你的DbContext OnModelCreating方法

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

    此过滤器将在每次查询宠物集合时应用,直接或通过OData的$expand。当然,您可以完全控制过滤器,您可以手动或有条件地禁用它——动态过滤器文档中有介绍。

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

    话虽如此,…看起来在OData之上有一个框架,它似乎还处于起步阶段,叫做RESTier,由微软开发。它似乎在OData之上提供了一个抽象层,支持这些类型的过滤器,如示例所示。

    这看起来就像上面的一个例子,在Domain对象中添加了一个过滤器:

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

    如果我要实现这个逻辑,我将回到这个答案来确认或否认这个框架的使用

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

    您的情况可能会有所不同,但这可能仍然是一个有用的框架。