Entity Framework does not translate Linq expression - c#

I have the following data model, and I need to group list of ResponseItem with these conditions:
First: Group by ResponseItem.Group
Second: Group by ResponseItem.SubGroup, but considering just the most recent one, which means considering the ResponseItem.CreationDate
Code:
public class ResponseItem
{
public string Group { get; set; }
public string SubGroup { get; set; }
public double Value { get; set; }
public DateTime CreationDate { get; set; }
}
public class GroupedResponseItem
{
public string Group { get; set; }
public List<ResponseItem> Items { get; set; }
}
The method is:
public List<GroupedResponseItem> GetGroupedData( IQueryable<ResponseItem> responseItems )
{
return responseItems
.OrderByDescending(i => i.CreationDate)
.GroupBy(i => i.Group)
.Select(grp => new GroupedResponseItem()
{
Group = grp.Key,
Items = grp
.GroupBy(i => new { i.SubGroup })
.Select(grp => grp.First())
.Select(i => new ResponseItem()
{
SubGroup = i.SubGroup,
CreationDate = i.CreationDate,
Value = i.Value
}).ToList()
})
.ToList();
}
But I get an error:
'The LINQ expression 'ProjectionBindingExpression: 0' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'
As I mentioned in the title, I'm using Entity Framework on .NET 6.
On the other hand, If I does not consider the second group by, query works fine:
public List<GroupedResponseItem> GetGroupedData(IQueryable<ResponseItem> responseItems)
{
return responseItems
.OrderByDescending(i => i.CreationDate)
.GroupBy(i => i.Group)
.Select(grp => new GroupedResponseItem()
{
Group = grp.Key,
Items = grp
.Select(i => new ResponseItem()
{
SubGroup = i.SubGroup,
CreationDate = i.CreationDate,
Value = i.Value
})
.ToList()
})
.ToList();
}

The culprit seems to be the secondary projection (Select) here
.GroupBy(i => new { i.SubGroup })
.Select(grp => grp.First()) // <-- (1)
.Select(i => new ResponseItem() // <-- (2)
{
SubGroup = i.SubGroup,
CreationDate = i.CreationDate,
Value = i.Value
})
.ToList()
While EF Core 6.0 has improved translation of GroupBy having additional operators on grouping result set (other than key/aggregates, which have natural SQL support), there are still limitations/defects preventing translation of some constructs. In particular, multiple projections.
Shortly, the Select after GroupBy must be the final LINQ operator. Which is kind of sad, since intermediate projection usually helps the translation and is often used to workaround EF Core limitations. But not in this case.
For this particular query, the projection looks redundant since the type of the elements of the group is the same as the projected type, so it could simply be removed
.GroupBy(i => new { i.SubGroup })
.Select(grp => grp.First()) // <-- final projection
.ToList()
So this is one of the solutions/workarounds. If you really need a projection, because you are selecting partial columns, or project to a different type, then move it inside the Select after GroupBy:
.GroupBy(i => new { i.SubGroup })
.Select(grp => grp
.Select(i => new ResponseItem()
{
SubGroup = i.SubGroup,
CreationDate = i.CreationDate,
Value = i.Value
})
.First()
) // <-- final projection
.ToList()

Related

EF: Get master-detail based on conditions on master as well as on detail

EF Core 3.1 and such master-detail model:
public class MyMaster
{
public Guid Id { get; set; }
public string Category { get; set; }
public List<MyDetail> MyDetails { get; set; }
}
public class MyDetail
{
public Guid Id { get; set; }
public Guid MasterId { get; set; }
public bool IsDeleted { get; set; }
}
I'd like to select all master records in certain category together with details not marked as deleted. Also if all details for particular master are marked as deleted then this master is not returned. Basically I'd like to run such simple select:
select * from MyMaster m
inner join MyDetail d on m.Id = d.MasterId and d.IsDeleted = 0
where m.Category = 'foo'
I try such LINQ method, but it returns also deleted detail records:
var result = await dbContext.MyMasters
.Include(m => m.MyDetails)
.Where(m => m.Category = 'foo')
.Join(dbContext.MyDetails.Where(d => !d.IsDeleted), m => m.Id, d => d.MasterId, (m, d) => m)
.ToListAsync();
How the LINQ method query should look like?
If you need data just for query, it can be easily retrieved by Select:
var result = await dbContext.MyMasters
.Where(m => m.Category = 'foo')
.Select(m => new MyMaster
{
Id = m.Id,
Category = m.Category,
MyDetails = m.MyDetails.Where(d => !d.IsDeleted).ToList()
})
.ToListAsync();
Your code is true bro, but I write and tested these queries and shown the result. its worked for me.
var result = await _dBContext.MyMasters
.Include(m => m.MyDetails)
.Where(m => m.Category == "foo")
.Join(_dBContext.MyDetails.Where(d => !d.IsDeleted),
m => m.Id,
d => d.MasterId,
(m, d) => new {
MasterId = m.Id,
Category = m.Category,
DetailId = d.Id,
IsDeleted = d.IsDeleted,
})
.ToListAsync();
var result2 = from m in _dBContext.MyMasters.Include(m => m.MyDetails)
join d in _dBContext.MyDetails on m.Id equals d.MasterId
where m.Category == "foo" && !d.IsDeleted
select new {
MasterId = m.Id,
Category = m.Category,
DetailId = d.Id,
IsDeleted = d.IsDeleted,
};
you try for select New type after Join two collection. IF you get Collection from MyDetails in masters, you have all items because it's not filtered, it's just all navigations item.
It looks like the actual question is how to filter the related entities.
Filtered Includes
In EF Core 5 and later this is done using filtered includes:
var result = await dbContext.MyMasters
.Include(m => m.MyDetails.Where(d=>!d.IsDeleted))
.Where(m => m.Category = 'foo')
.ToListAsync();
Global Query Filters
In EF Core 2 and late another option is to use global query filters to ensure deleted entities are never loaded through that DbContext, even by accident :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyDetail>().HasQueryFilter(p => !p.IsDeleted);
...
}
This way, the following query will only load active records :
var result = await dbContext.MyMasters
.Include(m => m.MyDetails)
.Where(m => m.Category = 'foo')
.ToListAsync();
A global query filter affects the entire DbContext which is perfectly fine. A DbContext isn't a model of the database, and entities aren't tables. It's perfectly fine to have different DbContexts with entities and different configurations targeting the same database, that handle specific scenarios/use cases (bounded contexts if you like DDD).
In this case most of the application would use a DbContext with the global query filter(s) while administrative pages would use a different one that allowed access to all records

Translating SQL into LinQ SYntax

Below are my intended SQL query and I am having a hard time translating this into LinQ Method Syntax
select top(2) MerchantSubcriptionName,count(*) as occurence
from MerchantSubscription
group by MerchantSubcriptionName
order by occurence desc
I am supposed to select the top 2 subscription which has the most people subscribed
Just try this:
public class Subscription
{
public string MerchantSubscriptionName { get; set; }
public int Count { get; set; }
}
var list = _dbContext.MerchantSubscription.GroupBy(x => x.MerchantSubcriptionName)
.Select(x => new Subscription { MerchantSubscriptionName = x.Key, Count = x.Count() })
.OrderByDescending(x => x.Count)
.Take(2)
.ToList();

I have a LINQ statement that is partially query and partially method. How can I do this using one or the other?

Here is my query as it stands now:
Goals = await (from p in _context.FixtureActivityTb
where p.ActivityType.Trim() == "G"
group p by p.PlayerId into x
join j in _context.PlayerTb on x.Key equals j.PlayerId
select new Stats
{
Name = j.GivenName,
pID = j.PlayerId,
TeamId = j.TeamId,
Count = x.Count()
})
.OrderByDescending(s => s.Count)
.ThenBy(s => s.Name)
.Take(10)
.ToListAsync();
As you can see this is a mix of method and query. I should be able to do a join, where, and add the data to a custom class all in Method, however so far I have not been able to put it all together. Any guidance will be appreciated.
I will include these other items, however, I think they are beside the point.
Variable Declaration:
public IList<Stats> Goals { get; set; }
Class:
public class Stats
{
public Guid pID { get; set; }
public string TeamId { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
If i understand your question (and the lack of coffee isn't affecting me), to get this all to a Linq chain method, it should be as simple as
Goals = await _context.FixtureActivityTb.Where(p => p.ActivityType.Trim() == "G")
.GroupBy(p => p.PlayerId)
.Join(_context.PlayerTb, x => x.Key, j => j.PlayerId, (x, j)
=> new Stats
{
Name = j.GivenName,
pID = j.PlayerId,
TeamId = j.TeamId,
Count = x.Count()
})
.OrderByDescending(s => s.Count)
.ThenBy(s => s.Name)
.Take(10)
.ToListAsync();
It's quite acceptable to mix the two, especially where you have a base query, and a paging part, as here. You can even compose the queries over multiple statements, eg:
var q = from p in _context.FixtureActivityTb
where p.ActivityType.Trim() == "G"
group p by p.PlayerId into x
join j in _context.PlayerTb on x.Key equals j.PlayerId
select new Stats
{
Name = j.GivenName,
pID = j.PlayerId,
TeamId = j.TeamId,
Count = x.Count()
};
Goals = await q.OrderByDescending(s => s.Count)
.ThenBy(s => s.Name)
.Take(10)
.ToListAsync();

Entity Framework select distinct values from one to many table with filter

I'm trying to write a query in EF.
Consider this relationship:
The goal is to get the teachers with the full collection of students, who are active between a certain filtered period (from-to).
I wrote the following query:
var filtered = _context.Teachers
.Join(_context.Students, // Target
fk => fk.Id, // FK
pk => pk.teacherId, // PK
(fk, pk) => new { teach = fk, students = pk }
)
.Where(i => i.students.date_active >= from &&
i.students.date_active <= to)
.OrderBy(i => i.teach.name)
.Select(i => i.teach)
.Include(i => i.Students)
.AsNoTracking();
With this query, I get duplicate teachers. So I'll just add the Distinct() operator and I have the teachers. But then my teacher object still contains all the students. I want only the students for that period. Any help on how to get the good result?
List<Dto.Teacher> list = filtered.Select(i => Dto.Teacher
{
id = i.Id,
name = i.name
Students = i.Students.Select(j => new Dto.Student
{
id = i.id,
date_active = i.date_active,
}).ToList(),
}).ToList();
public class Teacher()
{
public int id { get; set; }
public string name { get; set; }
public List<Dto.Student> Students { get; set; }
}
when using an ORM such as EF the join operator should be avoided as often as possible.
In your case you may try something like the following (variation are possible):
_context.Teachers.
Where(t => t.Sudents.Any(s => s.date_active >= from &&
s.date_active <= to)
).
Select(t => new {
teacher = t,
activeStudents = t.Students.Where(s => s.date_active >= from &&
s.date_active <= to)
});

Selecting a new type from linq query

I'm trying to transform some data selected out of a repository using Linq.
My code so far:
Repository<Result> _repository = new Repository<Result>();
var disciplines = _repository.Query()
.Select(d => new Discipline
{
DisciplineCode = d.DisciplineCode,
Name = d.DisciplineName
})
.Distinct()
.ToList();
Result class looks like:
public class Result
{
public virtual int ResultId { get; set; }
public virtual string DisciplineCode { get; set; }
public virtual string DisciplineName { get; set; }
public virtual int CompetitorId { get; set; }
//other stuff
}
When this runs, I get
Unable to determine the serialization information for the expression:
< MemberInitExpression >
Any idea what's going wrong?
EDIT:
As per Chris suggestion, I tried the Select after ToList like this:
var disciplines = _repository.Query()
.Select(d => new
{
DisciplineCode = d.DisciplineCode,
Name = d.DisciplineName
})
.Distinct()
.ToList()
.Select(d => new Discipline { DisciplineCode = d.DisciplineCode, Name = d.Name });
However, this time, similar error, but it's to do with the anonymous type:
Unable to determine the serialization information for the expression: new __AnonymousType(d.DisciplineCode, d.DisciplineName).
EDIT 2:
To clarify, .Query is returning IQueryable
The underlying database is MongoDB (using C# driver)
If I do this:
var disciplines = _repository.Query()
.Select(d => d.DisciplineName)
.Distinct()
.ToList()
It works. By works, I mean I get a distinct list of DisciplineNames
I need to be able to select more properties than just the name however.
I would suspect that your issue is that the MongoDB driver doesn't know how to create a Discipline object (or an anonymous object for that matter).
You need to move out of the the IQueryable<> and into IEnumerable<> to make this work.
Try this:
var disciplines =
_repository
.Query()
.ToArray()
.Select(d => new Discipline
{
DisciplineCode = d.DisciplineCode,
Name = d.DisciplineName
})
.Distinct()
.ToList();
The .ToArray() is the magic here. Let me know if this works.
You still may have issues with the .Distinct() call working on your custom type, so you may need to try this:
var disciplines =
_repository
.Query()
.ToArray()
.Select(d => new
{
d.DisciplineCode,
d.DisciplineName
})
.Distinct()
.Select(d => new Discipline
{
DisciplineCode = d.DisciplineCode,
Name = d.DisciplineName
})
.ToList();
Not sure why your first example does not run, it looks Ok, see related question here. Possibly it might be a bug in your db driver ?
You could try using GroupBy to achieve the same result:
var disciplines = _repository.Query()
.GroupBy(d => new Discipline
{
DisciplineCode = d.DisciplineCode,
Name = d.DisciplineName
}
)
.ToList();

Categories