WebAPI OData预过滤扩展查询

时间:2015-10-14 13:10:08

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

我想知道是否可以在扩展子句中的项目的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的查询宠物?$ expand = Pets

有没有办法检查&#34;宠物&#34;是一个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);
}

4 个答案:

答案 0 :(得分:5)

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

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

您可以选择实施自己的[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子句中,例如: GET 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
               ... */
            }
        }
    }

答案 1 :(得分:4)

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对象
  2. 见下文:

    var parser = new ODataUriParser(model, new Uri(serviceRootPath), requestUri);   
    var odataUri = parser.ParseUri();
    
    1. 创建一个方法,该方法将从根遍历到所有展开的实体,并通过ref传递ODataUri(以便在检查每个实体时根据需要更新它)
    2. 第一种方法将检查根实体,并根据根实体的属性添加任何其他过滤器。

      AddCustomFilters(ref ODataUri odataUri);
      

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

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

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

      1. 在检查每个实体时更新根过滤器
      2. 使用您的其他过滤器要求创建一个新的过滤器子句,并覆盖根级别的现有过滤器子句。 ODataUri根级别的$ filter有一个setter,因此可以覆盖它。

        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
        2. 创建新的Url

          见下文:

          var updatedODataUri = new Microsoft.OData.Core.UriBuilder.ODataUriBuilder(ODataUrlConventions.Default, odataUri).BuildUri();
          
          1. 将请求Uri替换为更新后的Uri。
          2. 这允许OData控制器使用更新的OData Url完成处理请求,其中包括您刚刚添加到根级别文件管理器的其他过滤器选项。

            ActionContext.Request.RequestUri = updatedODataUri;
            

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

            我希望这有助于您实现目标。

答案 2 :(得分:2)

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

在您的情况下,您将创建一个筛选出所有已删除记录的过滤器,例如:

您的DbContext OnModelCreating方法

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

每次您查询Pets系列时,都会应用此过滤器,直接或通过OData $ $扩展。您当然可以完全控制过滤器,您可以手动或有条件地禁用它 - 动态过滤器文档中对此进行了介绍。

答案 3 :(得分:1)

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

那就是说 ......看起来在OData之上有一个框架似乎处于由微软开发的名为RESTier的相对初期阶段。它似乎在OData之上提供了一个抽象层,允许这些类型的过滤器,如示例所示。

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

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

如果我开始实施这个逻辑,我将回到这个答案,以确认或否认使用这个框架。

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

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