1

I am doing a big database call for a shopping cart. It includes many relations, all specified with the .Include() method.

Now I only want EF to include what I have specified. When I include a collection, it automatically loads the collection's relations.

So I have a ShoppingCart, the shopping cart has a collection if ShoppingCartProducts and that one has a relation back to ShoppingCart and to Product.

So I want to include product, but not the shopping cart again, so I do:

IQueryable<ShoppingCart> query = DbContext.ShoppingCarts
                        .Include(p => p.ShoppingCartProducts)
                        .Include(p => p.ShoppingCartProducts.Select(x => x.Product))

Later I execute a .FirstOrDefault() which executes the query. Debugging through this, it has also included ShoppingCart within each ShoppingCartProducts.

This sounds a bit small, but it is actually this way throughout the application. New architecture turns entity objects into models with different static methods and extensions. Eventually causing an StackoverflowException because it recursively includes it's relations.

So how do I only Include what I have included?

I've turned LazyLoadingEnabled to false, and ProxyCreationEnabled to false. And my collections/reations are not marked with virtual.

Checked these answers:

DBContext lazyloadingenabled set to true still loads related entities by default It is true about the include on collections, but once a collection is included, that collection will load all other relations (I guess)

Entity Framework with Proxy Creation and Lazy Loading disabled is still loading child objects Almost same question, yet not an good answer, only an explanation

EF 6 Lazy Loading Disabled but Child Record Loads Anyway Using detached didn't help.

Edit:

As Evk mentioned, this has something to do with EF automatically filling up the blanks for already known relations. Question is now actually how to turn this off.

Edit 2:

So after an answer from Evk and my own workaround, we learn that these solutions don't solve the big picture. Let me try to explain:

These extensions and ConvertToModel methods are implemented in every repository and calling each other whenever it has a relation to it. The concept is actually great: Just convert to a model if you have the relation, if you have not, don't do anything. Yet because of this EF 'bug' I learn that all relations that are known inserted everywhere.

Here is an example where our solutions don't work. This for the case the code would call ConvertToModel for the ShoppingCart first, then the rest. But of course it could be visa-versa.

ShoppingCartRepository

    public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
    {
        if (entity == null) return null;
        ShoppingCartModel model = new ShoppingCartModel
        {
            Coupons = entity.ShoppingCardCoupons?.SelectShoppingCouponModel(typeof(ShoppingCart)),
            Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),

        };
        return model;
    }

ShoppingCartProductRepository

    public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);
        return source.Select(x => new ShoppingCartProductModel
        {
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
            ShoppingCartCoupons = includeRelations && objSource != typeof(ShoppingCartCoupon) ? x.ShoppingCartCoupons?.SelectShoppingCouponModel(typeof(ShoppingCartProduct)) : null,
        });
    }

ShoppingCartCouponRepository

    public static IEnumerable<ShoppingCartCouponModel> SelectShoppingCouponModel(this IEnumerable<ShoppingCartCoupon> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartCoupon>);
        return source.Select(x => new ShoppingCartCouponModel
        {
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
            ShoppingCartProduct = includeRelations && objSource != typeof(ShoppingCartProductModel) ? ShoppingCartProductRepository.ConvertToModel(x.ShoppingCartProduct) : null
        });
    }

When you study it, you will see it can go from ShoppingCart to ShoppingCartProduct to ShoppingCartCoupon back to ShoppingCart.

My current workaround will be to figure out the aggregate roots and choose which one needs which one. But I rather have an elegant solution to solve this. Best would be to prevent EF from loading those known relations, or somehow figure out if a property was loaded that way (reflection?).

Community
  • 1
  • 1
CularBytes
  • 9,924
  • 8
  • 76
  • 101
  • EF does not load related entities from database in your case. For each of the ShoppingCartProduct, related ShoppingCart is already known - that's the shopping cart you are making query for. This ShoppingCart is already loaded into the context, that is why EF fills ShoppingCart property of ShoppingCartProduct - it's already loaded and known and costs nothing to load. I don't know a way to somehow disable that (quite reasonable) behavior though. – Evk Feb 12 '17 at 16:20
  • Good point, this helps understand the 'problem'. Updated the answer. – CularBytes Feb 12 '17 at 16:38
  • I think you will have to modify your extension methods to prevent stack overflow by tracking entities you already visited and ignoring them (not converting to models). Or not ignoring but doing the same as EF does - substituting entities you had already converted to models. – Evk Feb 12 '17 at 16:52
  • @Evk, do you have a suggestion on how to do this nicely? – CularBytes Feb 12 '17 at 17:24
  • Would you mind using AutoMapper? Apart from fixing your issue it could also make your projections more efficient. – Gert Arnold Feb 12 '17 at 19:38

2 Answers2

1

As stated in comments, that's default behavior of entity framework and I don't think it can be changed. Instead, you can change your code to prevent stackoverflow exceptions. How to do that nicely is very dependent on your codebase, but I'll provide one sketch. In the sketch above I use other entity names (because I always check if my code samples at least compile before posting them here):

public static partial class Ex {
    public static CodeModel ConvertToModel(Code entity) {
        if (entity == null) return null;
        CodeModel model = new CodeModel();
        var map = new Dictionary<object, object>();
        map.Add(entity, model);
        model.Errors = entity.Errors?.SelectShoppingCartProductModel(map);
        return model;
    }        

    public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source, Dictionary<object, object> map = null) {
        bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ErrorModel {
            Code = includeRelations ? (map?.ContainsKey(x.Code) ?? false ? (CodeModel) map[x.Code] : ConvertToModel(x.Code)) : null,
            // other such entities might be here, check the map
        }).ToArray();
    }
}

Another option is to store current model in thread local variable. If you call some ConvertToModel method and this thread local variable is not null - that means this method has been called recursively. Sample:

public static partial class Ex {
    private static readonly ThreadLocal<CodeModel> _code = new ThreadLocal<CodeModel>();
    public static CodeModel ConvertToModel(Code entity) {
        if (entity == null) return null;
        if (_code.Value != null)
            return _code.Value;

        CodeModel model = new CodeModel();
        _code.Value = model;
        model.Errors = entity.Errors?.SelectShoppingCartProductModel();
        // other setters here
        _code.Value = null;
        return model;
    }

    public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source) {
        bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ErrorModel {
            Code = includeRelations ? ConvertToModel(x.Code) : null,
        }).ToArray();
    }
}

If you implement this in all your ConvertToModel methods - there is no need to pass any parameters or change other parts of your code.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • dayum, nice, but I was hoping for a bit more elegant solution that would avoid sending additional parameters. I now send the type of the object with it and use that to see if it is the same. I'm not sure but both solutions (yours and mine) seem to only solve the problem for 1 relation? – CularBytes Feb 12 '17 at 18:23
  • Well you can pass that map to all methods that perform conversions. Also you can combine all conversion methods in one non-static class and store map in private field of that class. This way no additional parameters needed. – Evk Feb 12 '17 at 18:31
  • thanks for the suggestion, better indeed but I'm afraid it doesn't solve the big picture. See updated answer. Not blaming you, thanks for the great help so far! I'm just looking deeper and deeper to solve this. – CularBytes Feb 12 '17 at 19:37
  • You need to be consistent. In one place you create ShoppingCartProductModel manually (SelectShoppingCartProductModel), in other place you call ShoppingCartProductRepository.ConvertToModel. If you will call ShoppingCartProductRepository.ConvertToModel everywhere - my last idea should work fine. – Evk Feb 12 '17 at 19:50
  • I have to support both converting a single object as well as a list. The `IEnumerable` extension can also be used if it is still a linq-to-sql, I have some benefits from that. Still a good solution though. – CularBytes Feb 14 '17 at 17:15
  • In your example you already use ConvertToModel inside IEnumerable conversions, so I doubt it can be used with linq to sql.However I understand your point. – Evk Feb 14 '17 at 18:03
  • That is where the `includeRelations` come into play :). – CularBytes Feb 14 '17 at 18:48
  • Then you can use the same check - if incude relations convert ienumerable using ConvertToModel. Otherwise convert manually (at this case all relations are null so no stackoverflow possible). Not super elegant but seems fine to me. – Evk Feb 14 '17 at 18:53
0

This solution checks if the source object type is not equal to the one we are calling ConvertToModel for.

public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
{
    if (entity == null) return null;
    ShoppingCartModel model = new ShoppingCartModel
    {
        ...
        Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),
    };
    return model;
}

and the SelectShoppingCartProductModel extension:

public static partial class Ex
{
    public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);//so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ShoppingCartProductModel
        {
            ....
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart)  ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
        });
    }
}

Yet this probably doesn't solve the entire problem. If you have another entity, let's say AdditionalCosts inside the ShoppingCart, that also has a reference to ShoppingCartProduct, it will still 'spin around'. If someone has a solution for this it would be great!

ShoppingCart -> ConvertToModel(shoppingCart) -> SelectAdditionalCostsModel -> ShoppingCartProduct -> ConvertToModel(shoppingCartProduct) -> ShoppingCart -> ConvertToModel(shoppingCart). And so on..

CularBytes
  • 9,924
  • 8
  • 76
  • 101