21

Let's say I have a method like this in one of my controllers:

[Route("api/Products")]
public IQueryable<Product> GetProducts() {
    return db.Products
             .Include(p => p.Category);
}

Using this I can get a product from the database and include its Category property.

In my CategoryControllerI have this method:

[Route("api/Categories")]
public IQueryable<Category> GetCategories() {
    return db.Categories
             .Include(c => c.Parent)
             .Include(c => c.Products)
             .Include(c => c.SubCategories);
}

When I send a GET request to the CategoryController this works as intended, I get the category, its parent, its products and its sub-categories. But when I send a GET request to the ProductController I don't want to include all the products in the category of the requested product, I just need the basic information about that category.

So, how can I make GetProducts() return the products in the database, including the Category property of each product, but excluding the Products list property of the category, still keeping the other properties like id, title and so on?

Thank you.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
tobloef
  • 1,821
  • 3
  • 21
  • 38
  • 1
    This looks more like a LINQ-to-SQL or Entity Framework issue rather than a ASP.NET issue. Please modify the tags and add the one for the ORM you are actually using. – Heinzi Aug 07 '16 at 15:47
  • @Heinzi Good call, updated the tags. – tobloef Aug 07 '16 at 15:49
  • Could you turn off Lazy Loading and use Eager loading? – Michael Aug 07 '16 at 16:06
  • @Michael Possibly, but I would rather not. I'll keep that ind mind as an alternative if no other solution comes up. EDIT: Wait, I'm pretty sure I'm already using eager loading? – tobloef Aug 07 '16 at 16:12
  • Remember that when your `IQueryable` gets serialized it will be iterated over, and all the properties will be "touched". If you have Lazy loading turned on, you will call the database for all relational properties (foreign keys). – Michael Aug 07 '16 at 16:16
  • Another way is to create a layer between the database and the service. So you would have a model for the database and a model for the service. You would then need to convert your database model into a DTO model. This would mean you have more control of what is being transferred. – Michael Aug 07 '16 at 16:18
  • @Michael As I've said in the edit to my comment, am I not already using eager loading? According to [this link] (https://msdn.microsoft.com/en-us/data/jj574232.aspx?f=255&MSPPError=-2147217396) "Eager loading is achieved by use of the Include method" which is what I'm currently doing. – tobloef Aug 07 '16 at 16:19
  • You are using eager loading, but that doesn't mean that lazy loading is turned off. The two methods can be used together. – Michael Aug 07 '16 at 16:20
  • @Michael I see. My previous comment about not wanting to disable lazy loading might not be valid then. I'll look further into it. – tobloef Aug 07 '16 at 16:21
  • Alright, let's say I disable Lazy Loading. How would I fix the problem that I'm having? The products in the category is still being loaded when I request a product. – tobloef Aug 07 '16 at 16:35

2 Answers2

22

As said in the comments, the first step is to disable lazy loading. You can either do that by removing the virtual modifier from the collection properties, which is permanent, or by disabling it per context instance, which is temporary:

context.Configuration.ProxyCreationEnabled = false;

(disabling proxy creation also disables lazy loading, but keeps the generated objects more light-weight).

In disconnected scenarios, like web API, people often prefer to disable lazy loading by default, because of this serializer-lazy-loading cascade.

However, you can't stop Entity Framework from executing relationship fixup. Loading a Productattaches it to the context. Include()-ing its categories attaches those to the context and EF populates their Products collections with the attached product, whether you like it or not. Circular references will still be a problem.

You can somewhat reduce this effect by fetching the products with AsNoTracking (which prevents entities to get attached, i.e. change-tracked):

return db.Products.AsNoTracking()
         .Include(p => p.Category);

Now categories will only have their Products filled with the Product of which they are the category.

By the way, in disconnected scenarios, also using AsNoTracking is preferred. The entities won't ever be saved by the same context instance anyway and it increases performance.

Solutions

  • Return DTOs, not entity types

By using DTO objects you take full control over the object graph that will be serialized. Lazy loading won't surprise you. But yeah, the amount of required DTO classes can be overwhelming.

  • Return anonymous types.

This will raise some eyebrows because we should never return anonymous types from methods, right? Well, they leave an action method as a Json string, just as named types, and the javascript client doesn't know the distinction. You might say that it only brings the weakly typed javascript environment one step closer. The only thing is that a named DTO type serves as a data contract (of sorts) and anonymous types can be changed (too) easily and break client-side code.

  • Tweak the serializer.

You can tell the Json.Net serializer to ignore reference loops. Using JsonConvert directly, it looks like so:

var products = db.Products.AsNoTracking().Include(p => p.Category);
var setting = new JsonSerializerSettings
{
    Formatting = Newtonsoft.Json.Formatting.Indented, // Just for humans
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var json = JsonConvert.SerializeObject(products, setting);

In combination with AsNoTracking() this will serialize the categories with empty Products arrays ("Products": []), because Product - Category - Product is a reference loop.

In Web API there are several ways to configure the built-in Json.Net serializer, you may want to do this per action method.

Personally, I prefer using DTOs. I like to be in control (also over the properties that cross the wire) and I don't particularly like to rely on a serializer to solve for me what I neglected to do.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • Thank you for the great answer. I went with the `AsNoTracking()` method in conjunction with `ReferenceLoopHandling = ReferenceLoopHandling.Ignore` which I was already using. If I ever need more control I might switch to DTOs, but right now AsNoTracking() seems to be working just fine. I will hold off on giving the bounty just yet, as I'm corious if any other answers will be posted, but I suspect you'll be the one getting it in the end. Once again, thank you. – tobloef Aug 14 '16 at 10:34
0

I believe it's not the best practice but it will work, you could create new object and fill it.

[Route("api/Products")]
public IQueryable<Product> GetProducts() {
    return db.Products
             .Include(p => p.Category);
             .Select(x => new Product{
                              Name = x.Name,
                              Price = x.Price,
                              Category = new Category{
                                          Name = x.Category.Name}})
}