CularBytes
CularBytes

Reputation: 10321

Only include what is included Entity Framework

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?).

Upvotes: 1

Views: 511

Answers (2)

Evk
Evk

Reputation: 101473

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.

Upvotes: 1

CularBytes
CularBytes

Reputation: 10321

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..

Upvotes: 0

Related Questions