关于c#:Web Api $ extend IQueryable带过滤器

Web Api $extend IQueryable with filter

我有一个设置,可以获得WebApi OData服务,该服务将返回:客户。返回客户的代码是:

1
2
3
4
public IHttpActionResult GetCustomers(ODataQueryOptions<Customer> queryOptions)
{
    return Ok(context.Customers.Where(i => i.IsActive).AsQueryable());
}

因此,GetCustomers方法返回所有活动客户的IQuerable结果。出于历史记录的目的,我们将所有客户保留在数据库中,但是当删除客户时,我们将IsActive字段设置为false。

使用一个简单的builder.EntitySet创建OData设置,以构建实体的Url。

1
EntitySetConfiguration<Customer> customers = builder.EntitySet<Customer>("customers");

这完美无瑕。我有一个Angular前端,它使用$ http调用来接收客户,等等。

但是,客户可以在数据库中包含相关联系人。要在Angular前端中获取联系人,我使用OData的$ extend功能:

1
odata/customers?$expand=contacts

这也很好。我收到所有相关联系人的客户。但是,正如您所猜测的那样,我只想接收应该返回IsActive的联系人。而且IQueryable功能可以将所有结果返回给我。

我知道我可以使用单独的Odata调用来获取联系人,但是我真的很想使用$ expand功能在一次调用中获取所有数据。我知道我也可以在客户端进行过滤(使用:$ filter)。但是我想在WebApi部分中正确设置此设置,因此客户端不必担心过滤掉不活动的结果。

我似乎无法弄清楚如何正确实现这一目标。有人可以帮助我走上正确的路吗?


EntityFramework.DynamicFilters是我所知道的最强大的Entity Framework工具之一。它跳入了经常被要求但直到EF6却从未实现过过滤后的Incude的功能的空白。它依靠EF的侦听API并在暴露非常简单的界面的同时进行了大量的修改表达式。

在您的情况下,您可以执行以下操作:

1
2
3
4
using EntityFramework.DynamicFilters;

// In OnModelCreating (DbContext)
modelBuilder.Filter("CustomerActive", (Customers c) => c.IsActive);

仅此而已!现在,无论是通过导航属性还是在Include中直接查询Customers的地方,都会将谓词添加到查询中。

您要所有客户吗?您只需执行

,就可以针对每个上下文实例关闭过滤器

1
context.DisableFilter("CustomerActive");

到目前为止,我只发现了一个小故障(或警告)。如果有两个实体ParentChild,并且Parent上有一个不返回任何记录的过滤器,则此查询...

1
context.Children.Include(c => c.Parent)

...不返回任何内容。但是,根据查询的形状,我希望它返回具有空父级的Child实体。

这是因为在SQL中,在ParentChild之间有一个INNER JOIN,并且在Parent上的谓词的值是falseOUTER JOIN会给出预期的行为,但是我们当然不能要求该库具有这种智能。


数据模型:

1
2
3
4
5
6
7
8
9
10
11
12
public class Customer
{
    public int Id { get; set; }
    public bool IsActive { get; set; }
    public ICollection<Contact> Contacts { get; set; }
}

public class Contact
{
    public int Id { get; set; }
    public bool IsActive { get; set; }
}

具有固定数据的控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomersController : ODataController
{
    private List<Customer> customers = new List<Customer>
    {
        new Customer { Id = 1, IsActive = false },
        new Customer { Id = 2, IsActive = true,
            Contacts = new List<Contact>
            {
                new Contact { Id = 101, IsActive = true },
                new Contact { Id = 102, IsActive = false },
                new Contact { Id = 103, IsActive = true },
            }
        }
    };

    [EnableQuery]
    public IHttpActionResult Get()
    {
        return Ok(customers.Where(c => c.IsActive).AsQueryable());
    }
}

请注意,一个客户处于活动状态,并且该客户具有2个(共3个)活动联系人。

最后,配置您的OData服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<Customer>("customers");

        config.MapODataServiceRoute(
            routeName:"OData",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

现在按以下方式调用服务:

1
GET http://host/customers?$expand=Contacts($filter=IsActive eq true)

您应该收到类似于以下内容的有效负载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
 "@odata.context":"http:/host/$metadata#customers",
 "value": [
    {
     "Id": 2,
     "IsActive": true,
     "Contacts": [
        {
         "Id": 101,
         "IsActive": true
        },
        {
         "Id": 103,
         "IsActive": true
        }
      ]
    }
  ]
}


一种可能的解决方案是添加视图以表示您实际要公开的数据。

您可以拥有客户视图和联系人视图,这些视图只是原始表的过滤版本。

回到C#端,您的模型可以直接引用视图,就像它们是表一样。

令人高兴的是,它们将像表一样对待,所有延迟加载,导航属性和数据库端过滤仍然可以像引用原始表一样工作。