简体   繁体   English

WebAPI OData预过滤扩展查询

[英]WebAPI OData pre filtering expand queries

I want to know if it's possible to pre-filter OData results in a WebAPI for items in the expand clause. 我想知道是否可以在扩展子句中的项目的WebAPI中预先过滤OData结果。 I only want this to filter based on a predefined interface with a Deleted flag. 我只希望根据带有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();
    }
}

You can see that I'm very easily filtering deleted people. 你可以看到我很容易过滤被删除的人。 The problem comes when someone gets deleted Pets from a query like /api/Persons?$expand=Pets 有人被删除的问题来自/ api / Persons这样的查询宠物?$ expand = Pets

Is there a way to check if this expansion of "Pets" is an IDbDeletedDateTime and filter them accordingly? 有没有办法检查“Pets”的扩展是否为IDbDeletedDateTime并相应地过滤它们? Maybe there is a better way to approach this? 也许有更好的方法来解决这个问题?

EDIT: 编辑:

I tried to solve this based on what was picked up in this answer . 我试着根据这个答案中提到的内容来解决这个问题 I don't think it can be done, at least not in all scenarios. 我不认为可以做到,至少在所有情况下都不行。 The only part of a ExpandedNavigationSelectItem that even looks like it is related to the filters is the FilterClause . ExpandedNavigationSelectItem中唯一看起来与过滤器相关的部分是FilterClause This can be null when it has no filter, and it is only a getter property , meaning we can't set it with a new filter if we wanted to. 当它没有过滤器时,它可以为null, 并且它只是一个getter属性 ,这意味着如果我们想要的话,我们不能用新的过滤器设置它。 Weather or not it is possible to modify a current filter is only covering a small use case that I'm not particularly interested in if I can't add a filter freshly. 天气与否可以修改当前过滤器只涵盖一个我不是特别感兴趣的小用例,如果我不能新添加过滤器。

I have an extension method that will recurse through all the expand clauses and you can at least see what the FilterOption is for each expansion. 我有一个扩展方法,它将遍历所有的扩展子句,你至少可以看到每个扩展的FilterOption是什么。 If anyone can get this 90% code fully realized, that would be amazing, but I'm not holding my breath on it. 如果有人能够完全实现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);
}

Correct me if I understood wrong: you want to always filter the entities if they implement the interface IDbDeletedDateTime , so when the user wants to expand a navigation property you also want to filter if that navigation property implements the interface, right? 如果我理解错误,请纠正我:如果他们实现接口IDbDeletedDateTime ,你想要总是过滤实体,所以当用户想要扩展导航属性时,如果导航属性实现了接口,你也想过滤,对吧?

In your current code you enabled OData query options, with the [EnableQuery] attribute, so OData will handle the expand query option for you, and the Pets will not be filtered the way you want. 在您当前的代码中,您使用[EnableQuery]属性启用了OData查询选项,因此OData将为您处理扩展查询选项,并且不会按您希望的方式过滤宠物。

You have the option of implementing your own [MyEnableQuery] attribute, and override the ApplyQuery method: check there if the user has set the $expand query option and if so, check if the requested entity implements IDbDeletedDateTime and filter accordingly. 您可以选择实现自己的[MyEnableQuery]属性,并覆盖ApplyQuery方法:检查用户是否设置了$ expand查询选项,如果是,请检查请求的实体是否实现了IDbDeletedDateTime并相应地进行过滤。

You can check here the code of the [EnableQuery] attribute and see that in the ApplyQuery method you have access to the object ODataQueryOptions that will contain all the query options set by the user (WebApi populates this object from the URI query string). 您可以在此处查看[EnableQuery]属性的代码,并在ApplyQuery方法中查看您可以访问对象ODataQueryOptions ,该对象将包含用户设置的所有查询选项(WebApi从URI查询字符串填充此对象)。

This would be a generic solution that you could use in all your controller methods if you are going to have several entities with that interface with your custom filtering. 这将是一个通用的解决方案,如果您要在自定义过滤中使用该接口的多个实体,则可以在所有控制器方法中使用。 If you only want this for a single controller method, you can also remove the [EnableQuery] attribute, and invoke the query options directly in the controller method: add the ODataQueryOptions parameter to your method and handle the query options manually. 如果您只希望将其用于单个控制器方法,则还可以删除[EnableQuery]属性,并直接在控制器方法中调用查询选项:将ODataQueryOptions参数添加到方法中并手动处理查询选项。

That would be something like: 这将是这样的:

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

See the section Invoking Query Options directly to understand more how to play around with that object. 请参阅直接调用查询选项部分以了解有关如何使用该对象的更多信息。 If you read the entire article, be aware that the [Queryable] attribute is your [EnableQuery] attribute, since the article is from a lower version of OData. 如果您阅读整篇文章,请注意[Queryable]属性是您的[EnableQuery]属性,因为该文章来自较低版本的OData。

Hope it points you in the right direction to achieve what you want ;). 希望它指出你正确的方向,以实现你想要的;)。


EDIT : some information regarding nested filtering in $expand clause: 编辑 :有关$ expand子句中嵌套过滤的一些信息:

OData V4 supports filtering in expanded content. OData V4支持在扩展内容中进行过滤。 This means you can nest a filer inside an expand clause, something like: GET api/user()?$expand=followers($top=2;$select=gender). 这意味着你可以在扩展子句中嵌入一个文件管理器,例如:GET api / user()?$ expand = followers($ top = 2; $ select = gender)。 In this scenario, again you have the option to let OData handle it, or handle it yourself exploring the ODataQueryOptions parameter: Inside your controller you can check expand options and if they have nested filters with this code: 在这种情况下,您再次可以选择让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, I had a similar requirement and I was able to solve it by writing an algorithm that adds additional filtering to the request ODataUri based on the properties of my model. Zachary,我有类似的要求,我能够通过编写一个算法来解决它,该算法根据模型的属性为请求ODataUri添加额外的过滤。 It examines any properties at the root level entity and the properties of any expanded entities as well to determine what additional filter expressions to add to the OData query. 它会检查根级实体的任何属性以及任何已扩展实体的属性,以确定要添加到OData查询的其他过滤器表达式。

OData v4 supports filtering in $expand clauses but the filterOption in the expanded entities is read only so you cannot modify the filter expressions for the expanded entities. OData v4支持在$ expand子句中进行过滤,但扩展实体中的filterOption是只读的,因此您无法修改扩展实体的过滤器表达式。 You can only examine the filterOption contents at the expanded entities. 您只能检查扩展实体的filterOption内容。

My solution was to examine all entities (root and expanded) for their properties and then add any additional $filter options I needed at the root filter of the request ODataUri. 我的解决方案是检查所有实体(根和扩展)的属性,然后在请求ODataUri的根过滤器中添加我需要的任何其他$ filter选项。

Here is an example OData request Url: 以下是OData请求Url的示例:

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

This is the same OData request Url after I had updated it: 我更新后,这是相同的OData请求Url:

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

Steps to accomplish this: 完成此任务的步骤:

  1. Use ODataUriParser to parse the incoming Url into a Uri object 使用ODataUriParser将传入的Url解析为Uri对象

See below: 见下文:

var parser = new ODataUriParser(model, new Uri(serviceRootPath), requestUri);   
var odataUri = parser.ParseUri();
  1. Create a method that will traverse down from the root to all expanded entities and pass the ODataUri by ref (so that you can update it as needed as you examine each entity) 创建一个将从根遍历到所有扩展实体的方法,并通过ref传递ODataUri(以便在检查每个实体时根据需要更新它)

The first method will examine the root entity and add any additional filters based on the properties of the root entity. 第一种方法将检查根实体,并根据根实体的属性添加任何其他过滤器。

AddCustomFilters(ref ODataUri odataUri);

The AddCustomFilters method will the traverse the expanded entities and call the AddCustomFiltersToExpandedEntity which will continue to traverse down all expanded entities to add any necessary filters. AddCustomFilters方法将遍历展开的实体并调用AddCustomFiltersToExpandedEntity ,它将继续遍历所有展开的实体以添加任何必要的过滤器。

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

The method AddCustomFiltersToExpandedEntity should call itself as it loops over the expanded entities at each level. AddCustomFiltersToExpandedEntity方法应该调用自身,因为它循环遍历每个级别的展开实体。

  1. To update the root filter as you examine each entity 在检查每个实体时更新根过滤器

Create a new filter clause with your additional filter requirements and overwrite the existing filter clause at the root level. 使用其他筛选器要求创建新的筛选器子句,并覆盖根级别的现有筛选器子句。 The $filter at the root level of the ODataUri has a setter so it can be overriden. ODataUri根级别的$ filter有一个setter,因此可以覆盖它。

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

Note: I suggest creating a new filter clause using a BinaryOperatorKind.And so that your additional filter expressions are simply appended to any existing filter expressions already in the ODataUri 注意:我建议使用BinaryOperatorKind.And创建一个新的过滤器子句,以便将您的其他过滤器表达式简单地附加到ODataUri中已有的任何现有过滤器表达式

var combinedFilterExpression = new BinaryOperatorNode(BinaryOperatorKind.And, odataUri.Filter.Expression, newFilterExpression);
odataUri.Filter = new FilterClause(combinedFilterExpression, newFilterRange);
  1. Use ODataUriBuilder to create a new Url based on the updated Uri 使用ODataUriBuilder根据更新的Uri创建新的Url

See below: 见下文:

var updatedODataUri = new Microsoft.OData.Core.UriBuilder.ODataUriBuilder(ODataUrlConventions.Default, odataUri).BuildUri();
  1. Replace the request Uri with the updated Uri. 用更新的Uri替换请求Uri。

This allows the OData controller to complete processing the request using the updated OData Url which includes the additional filter options you just added to the root level filer. 这允许OData控制器使用更新的OData Url完成处理请求,其中包括您刚添加到根级别文件管理器的其他过滤器选项。

ActionContext.Request.RequestUri = updatedODataUri;

This should provide you with the capability to add any filtering options you need and be 100% sure that you have not altered the OData Url structure incorrectly. 这应该为您提供添加所需的任何过滤选项的能力,并且100%确保您没有错误地更改OData Url结构。

I hope this helps you achieve your goals. 我希望这可以帮助您实现目标。

I had a similar problem and I managed to solve it using Entity Framework Dynamic Filters . 我有类似的问题,我设法使用Entity Framework动态过滤器解决它。

In your case, you would create a filter that filters out all deleted records, like that: 在您的情况下,您将创建一个筛选出所有已删除记录的筛选器,如下所示:

Your DbContext OnModelCreating method 你的DbContext OnModelCreating方法

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

This filter will be then applied every time you query your Pets collection, either directly or trough OData's $expand. 每次您查询您的Pets系列时,都会应用此过滤器,直接或通过OData $ $扩展。 You have of course full control over the filter, and you can disable it manually or conditionally - it is covered in the Dynamic Filters documentation. 您当然可以完全控制过滤器,您可以手动或有条件地禁用它 - 动态过滤器文档中对此进行了介绍。

I asked the OData team about this issue, and I may have an answer that can be used. 我向OData团队询问了这个问题,我可能会有一个可以使用的答案。 I haven't been able to test it out fully and get it used, but it looks like it will solve my problems when I am able to get around to them. 我无法完全测试并使用它,但看起来它能解决我的问题,当我能够解决它们时。 I want to post this answer just in case this will help someone else. 我想发布这个答案,以防万一这会帮助别人。

That being said ... It looks there is a framework on top of OData that seems to be in its relative infancy called RESTier being developed by Microsoft. 话虽如此 ......看起来OData上面有一个框架似乎处于相对初期,称为RESTier ,由微软开发。 It seems to offer a layer of abstraction on top of OData that allows for these kinds of filters, as the examples would suggest. 它似乎在OData之上提供了一个抽象层,允许这些类型的过滤器,如示例所示。

This looks like it would be an example above with a filter in the Domain object that would be added: 这看起来像上面的示例,在Domain对象中将添加一个过滤器:

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

If I get around to implementing this logic, I'll return to this answer to confirm or deny the use of this framework. 如果我开始实现这个逻辑,我将回到这个答案,以确认或否认使用这个框架。

I was never able to implement this solution to know if it's worthwhile. 我从来没有能够实现这个解决方案,知道它是否值得。 There were too many challenges to justify the worth in my particular use case. 在我的特定用例中,有太多的挑战需要证明其价值。 It may very well be a great solution for new projects or folks the really need these features, but my particular use case was challenging to implement the framework into existing logic. 对于真正需要这些功能的新项目或人来说,它可能是一个很好的解决方案,但我的特定用例很难将框架实现到现有逻辑中。

Your mileage may vary, and this may still be a useful framework to check out. 您的里程可能会有所不同,这可能仍然是一个有用的框架来检查。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM