3

I have a setup where I've get a WebApi OData service which returns: Customers. The code for returning the customers is:

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

So the GetCustomers method returns an IQuerable result of all active customers. For history purposes we leave all customers in the database, but when a customer is removed, we set the IsActive field to false.

The OData setup is created using a simple builder.EntitySet to build the Url for the entities.

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

This works flawlessly. I have an Angular front-end which uses $http calls to receive the customers, etc.

However a customer can contain related contacts in the database. To get the contacts in the Angular Frontend, I use the $extend functionality of OData:

odata/customers?$expand=contacts

This also works great. I receive the customers with all related contacts. However as you've guessed I would like to receive only contacts which have IsActive should be returned. And the IQueryable functionality gives me all results back.

I understand I can use the seperate Odata call to get the contacts, but I really would like to use the $expand features to get all data in one call. I know I can also do the filtering on the client side (with: $filter). But I'd like to setup this correctly in the WebApi part, so the client does not have to care about filtering inactive results back.

I can't seem to figure out how to achieve this correctly. Can somebody help me get on the right track?

Rody
  • 2,675
  • 1
  • 22
  • 40
  • You can do this with Eager loading perhaps? https://msdn.microsoft.com/en-us/data/jj574232.aspx?f=255&MSPPError=-2147217396 – Alexander Derck Dec 07 '15 at 13:20
  • Alexander, that could be a solution, however, I would not like to get the contacts every time when receiving the customers. – Rody Dec 07 '15 at 13:24
  • Could you tried to return `IQueryable`? – csharpfolk Dec 13 '15 at 17:35
  • [EntityFramework.DynamicFilters](https://github.com/jcachat/EntityFramework.DynamicFilters) is what you need. – Gert Arnold Dec 13 '15 at 20:11
  • I had a similar problem a while back, and [wrote my own crazy bit of code to solve it](http://stackoverflow.com/questions/32845222/controlling-what-is-returned-with-an-expand-request/32945237#32945237). A link is not a valid answer, however, so I can't post it as such here. – Alex Dec 15 '15 at 16:22
  • @GertArnold your comment is exactly what I needed and requested! That package fits perfectly. I cannot give the bounty to a comment. If you post your comment as an answer I give you the bounty. – Rody Dec 17 '15 at 10:36

3 Answers3

5

EntityFramework.DynamicFilters is one of the greatest tools for Entity Framework that I know. It jumped into the gap of the often-requested but up to EF6 never implemented feature of filtered Incudes. It leans on EF's interception API and does the heavy lifting of modifying expressions while exposing a very simple interface.

In your case, what you can do is something like this:

using EntityFramework.DynamicFilters;

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

That's all! Now everywhere where Customers are queried, be it directly, through navigation properties or in Includes, the predicate will be added to the query.

Do you want all customers? You can simply turn the filter off per context instance by doing

context.DisableFilter("CustomerActive");

There's only one glitch (or caveat) I discovered so far. If there are two entities, Parent and Child and there is a filter on Parent that doesn't return any records, then this query ...

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

... doesn't return anything. However, from the shape of the query, I would expect it to return Child entities with empty parents.

This is because in SQL there is an INNER JOIN between Parent and Child and a predicate on Parent that evaluates to false. An OUTER JOIN would give the expected behavior, but of course we can't demand from this library to be that smart.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
2

Data model:

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; }
}

Controller with canned data:

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());
    }
}

Note that one Customer is active, and that Customer has 2 (out of 3) active Contacts.

Finally, configure your OData service:

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());
    }
}

Now call the service as follows:

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

You should receive a payload similar to this:

{
  "@odata.context": "http:/host/$metadata#customers",
  "value": [
    {
      "Id": 2,
      "IsActive": true,
      "Contacts": [
        {
          "Id": 101,
          "IsActive": true
        },
        {
          "Id": 103,
          "IsActive": true
        }
      ]
    }
  ]
}
lencharest
  • 2,825
  • 2
  • 15
  • 22
  • However this is a very complete answer, I already thought of this, but I would like to perform the action on the server side, not on a client side $filter. See my last sentence in the question: "I know I can also do the filtering on the client side (with: $filter). But I'd like to setup this correctly in the WebApi part, so the client does not have to care about filtering inactive results back." – Rody Dec 17 '15 at 10:37
  • 1
    The filtering _is_ happening on the server side. The client is simply requesting a certain kind of filtering with the `$expand=Contacts($filter=IsActive eq true)` expression. Are you saying you don't want to have to add that expression to the request URI? – lencharest Dec 17 '15 at 20:13
  • You are right that the filtering is done on the server yes. Maybe I phrased it not clear enough, I would like the client side (which generates the URI's) to not know about the IsActive field. The DynamicFilters solution is the best option for me. – Rody Dec 18 '15 at 09:10
0

One possible solution is to add Views to represent the data you actually want to expose.

You can have Customer and Contact Views which are just filtered versions of the original table.

Back on the C# side, your models can directly reference the Views as if they were tables.

The nice thing is that they will be treated just as tables, all lazy loading, navigation properties, and database side filtering will still work as if you were referencing the original tables.

hatcyl
  • 2,190
  • 2
  • 21
  • 24
  • It could be a viable option to use views, but I don't like the general feeling of the idea of using views in Entity Framework code first. I think using the solution from @GertArnold is better suited to my question. – Rody Dec 17 '15 at 10:39