MapsterMapper / Mapster

A fast, fun and stimulating object to object Mapper
MIT License
4.37k stars 332 forks source link

EF Core and ProjectToType eager loader and is ignoring include #468

Closed kavatari closed 3 months ago

kavatari commented 2 years ago

Following the next example: when using ProjectToType on a queryable Mapster is eager loading the data "SomeOtherEntity" and ignores the "include take(1)".

class SomeEntity 
{
    private ICollection<SomeOtherEntity> MyList { get; set; }
}

class SomeOtherEntity
{
    private SomeEntity myEntity { get; set; }
}

class MyModel 
{
    private ICollection<SomeOtherEntity> MyList { get; set; }
}

class MyOtherModel
{
    private MyModel myEntity { get; set; }
}

await _dbContext.SomeEntity
    .AsNoTracking()
    .Where(x => x.IsActive == isActive)
    .Include(x => 
        x.SomeOtherEntity
            .OrderByDescending(p => p.Date)
            .Take(1))
    .ProjectToType<MyModel>()
    .ToListAsync(cancellationToken);

Mapping data this way: works fine!

var data = await _dbContext.SomeEntity
            .AsNoTracking()
            .Where(x => x.IsActive == isActive)
            .Include(x => 
                x.SomeOtherEntity
                    .OrderByDescending(p => p.Date)
                    .Take(1))
            .ToListAsync(cancellationToken);

        return data.Adapt<List<SomeModel>>();
heidgert commented 2 years ago

I have tried something similar and noticed that the OrderBy on the SomeOtherEntity is not being executed in the query either.

jewijaya commented 1 year ago

I also have some issue when the intention not to include some lazy loading property but eventually it loaded when use ProjectToType.

brgrz commented 1 year ago

This is a problem I too just noticed today when I saw the SELECT statement being generated with joins and I didn't do the joins/includes myself and I know EF Core doesn't eager load either unless the entities are already being tracked.

I will assume fixing this for all the use cases of queryable will prove hard and I'm not sure a mapper should be even doing that. As an optional, opt-in (i.e. explicit) feature, maybe but definitely not by default.

Keseven commented 9 months ago

Can confirm that using ProjectToType<> will remove the include "where" etc. This is pretty serious as the code compiles and runs without any warning that this will happen.

Is this considered expected behaviour or a bug that could be fixed?

DisturbedNeo commented 9 months ago

Updated to the latest version of Mapster recently and some of my tests suddenly started failing. Even just simply mapping a table to an output with no other methods doesn't work now.

public async Task<List<TOutput>> GetAllAsync<TEntity, TOutput>() where TEntity : class
{ 
    return await _context.Set<TEntity>().ProjectToType<TOutput>().ToListAsync();
}

Even with this oversimplified use case, no matter how many records the table has, this always returns an empty list. But, as mentioned above, doing:

public async Task<List<TOutput>> GetAllAsync<TEntity, TOutput>() where TEntity : class
{ 
    var entities = await _context.Set<TEntity>().ToListAsync();
    return entities.Adapt<List<TOutput>>();
}

Works just fine, so there's definitely an issue with the ProjectToType<>() method.

bryanllewis commented 3 months ago

My tests are also failing because ProjectToType is eager loading all of my nav properties. I had to take this out of my project because it's causing problems. This is definitely the issue, because removing it immediately returned everything to the expected behavior.

andrerav commented 3 months ago

This is a known issue with Mapster and EF which comes up frequently. The workaround is to materialize your queries with ToList()/ToListAsync() before calling ProjectToType(). I will update the documentation to reflect this.

bryanllewis commented 3 months ago

This is a known issue with Mapster and EF which comes up frequently. The workaround is to materialize your queries with ToList()/ToListAsync() before calling ProjectToType(). I will update the documentation to reflect this.

@andrerav doesn't this sort of defeat the purpose of the IQueryable extension? Or am I misunderstanding your suggestion? If you materialize the query first and then do ProjectToType on that, that's no different than just a regular "after the fact" mapping (get the entire entity fromt eh DB, then map it to the DTO). The value in the ProjectToType is that it acts on the Queryable and generates a SQL query that only selects DTO fields.

ProjectToType is an IQueryable extension, if we call ToList, then we would have to call myList.AsQueryable().ProjectToType() just to get it to work.