LINQ include and Projection - linq

I have some classes defining entities with relationships
Account
has many Conversations [IEnumerable<Conversation> Conversations]
Conversation
has many Participants [IEnumerable<Account> Participants]
has many Messages [IEnumerable<Message> Messages]
Message
has one Sender [Account Sender]
has one Conversation [Conversation Conversation]
I'm trying to write a LINQ query that returns a list of Conversation ordered by date and including related participants and messages.
public async Task<List<Conversation>> FindAllByAccountIdAsync(Int32 id)
{
return await _Db.Conversations
.Where(c => c.Participants.Any(p => p.AccountId == id))
.Include(c => c.Participants)
.Include(c => c.Messages)
.ToListAsync();
}
This do the work but includes to much data i do not really need.
public async Task<List<Conversation>> FindAllByAccountIdAsync(Int32 id)
{
return await _Db.Conversations
.Where(c => c.Participants.Any(a => a.AccountId == id))
.Include(c => c.Participants.Select(a=> new
{
AccountId = a.AccountId,
Profile = new { FullName = a.Profile.FullName,
Email = a.Profile.Email
}
}))
// Only return the last message in
// Eventually I would not return an array with a single object but just the single object inside associated with the property LastMessageIn
.Include(c => c.Messages.OrderBy(m => m.Date).Select(m=> new
{
Body = m.Body,
SenderId = m.Sender.AccountId
}).Last())
.ToListAsync();
}
This script returns a mile long exception
{"message":"An error has occurred.","exceptionMessage":"The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties........}
My mind resist understanding and learning LINQ I do not know if its just me but as soon requirements exceeds basic querying and projection it break out of my control
Someone have some hints?

I'm not sure if I understand your question, but I believe you want something like this:
public async Task<List<Conversation>> FindAllByAccountIdAsync(Int32 id)
{
return await _Db.Conversations
.Where(c => c.Participants.Any(p => p.AccountId == id))
.Include(c => c.Participants)
.Include(c => c.Messages)
.Select(c => new
{
Participants = c.Participants.Select(a=> new
{
AccountId = a.AccountId,
Profile = new { FullName = a.Profile.FullName,
Email = a.Profile.Email
}
},
//EDIT: using OrderByDescending and FirstOrDefault
Messages = c.Messages.OrderByDescending(m => m.Date).Select(m=> new
{
Body = m.Body,
SenderId = m.Sender.AccountId
}).FirstOrDefault())
//others properties here
}
.ToListAsync();
}

You cannot project on an Include. An include is simply Eager Loading. The output does not change in the C#. Only the amount of data that is originally loaded (ie performance) changes.
It seems you want a projection and not eager loading, which are completely incompatible concepts.
However I cannot understand what exactly what you are trying to achieve.

public async Task<List<Conversation>> FindAllByAccountIdAsync(Int32 id)
{
return await _Db.Conversations
.Where(c => c.Participants.Any(p => p.AccountId == id))
.Include(c => c.Participants.Select(_=>_))
.Include(c => c.Messages.Select(_=>_))
.ToListAsync();
}
Should be enough.

Related

How to Filter Records After GroupBy with LINQ + EF Core 6

Given this simple Contract entity and assuming that a customer can have many Contracts with different start dates and statuses.
public class Contract {
[Key]
int ContractId { get; set; }
int CustomerId { get; set; }
string Status { get; set; }
DateTime ContractStartDate { get; set; }
}
I'm trying to write a query to find that latest contract for each customer and then filter out certain statuses. I need to filter the status after finding the latest contract, to ensure I'm getting their current status.
ContractID
CustomerID
ContractStartDate
Status
1
1
2022-01-01
Active
2
1
2022-31-05
Inactive
3
2
2022-01-03
Active
4
2
2022-31-07
Inactive
From the above data set, I would expect to get contracts 2 and 4 in the results. This is why I can't filter on status before grouping, because then the latest inactive row would be eliminated before I group them by customer to find the latest row.
I've tried something like, this:
var latestContracts = Query<Contract>()
.GroupBy(grp => grp.CustomerId)
.Select(s => s.OrderByDescending(s => s.ContractStartDate).First())
.Where(w => w.Status == "Active");
but once I execute the query by calling ToListAsync(), etc. I get an error like this:
'The LINQ expression 'DbSet<Customer>()
.GroupBy(c => c.CustomerId)
.Select(g => g
.AsQueryable()
.OrderByDescending(e => e.
I CC ContractStartDate)
.First())
.Where(e0 => e0.AccountContractTermStatus == "Active")' 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'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
If I remove the Where call, the query works fine and gives the intended newest results. I know that the support for translating LINQ to SQL is still not fully complete, even in EF Core 6, but this seems like something that should be possible. Am I missing something simple or is this just not yet doable via LINQ?
Happy to provide more context, as I've greatly simplified a production query into something I could share publicly.
Try the following emulation of query which EF Core cannot translate. It is almost direct translation what EF Core should do in your case.
var dataQuery = Query<Contract>();
var latestContracts =
from d in dataQuery.Select(d => new { d.CustomerId }).Distinct()
from c in dataQuery
.Where(c => d.CustomerId == c.CustomerId)
.OrderByDescending(c => c.ContractStartDate)
.Take(1)
where c.Status == "Active"
select c;
Or using Method Chain syntax:
var dataQuery = Query<Contract>();
var latestContracts = dataQuery
.Select(d => new { d.CustomerId })
.Distinct()
.SelectMany(d => dataQuery
.Where(c => d.CustomerId == c.CustomerId)
.OrderByDescending(c => c.ContractStartDate)
.Take(1))
.Where(c => c.Status == "Active");

How to use linq to iterate over all images but only search a part of the string in those images?

I have a collection of images. All of them are named according to what they used for.
for logos they follow the convention business_logo, for logbooks they follow a convention business_logbook.
Now I want to write a LinQ iterating over all images, find the one that contains (_logbook), so I can delete the image in that logbook. If I remove it from the context it will remove it from wwwroot automatically.
Also the query needs to be async.
You select the items where the path contains the string you are searching for:
public class Image
{
public string Name { get; set; }
}
var _imageRepo = new Collection<Image>()
{
new Image { Name = "business_logo_123954.png" },
new Image { Name = "business_logbook_529429.png" },
new Image { Name = "business_logo_81957.png" },
new Image { Name = "business_logbook_234820.png" }
};
var logBookImages = await _imageRepo
.Where(x => x.Name.Contains("_logbook"))
.ToListAsync();
// Or you can check that the file name starts with business_logbook.
var logBookImages = await _imageRepo
.Where(x => x.Name.StartsWith("business_logbook"))
.ToListAsync();
Linq queries are neither async or sync. A query is not executed until you call one of the various materialization methods on the query. In your case, since you want the data to mbe materialized async, you'll need to call .ToListAsync() instead of .ToList(). Doing so, you'll also need to add the await keyword to the statement.
I would suggest getting your collection of items first and then deleting them, but it's a matter of preference and what database system/pattern you're using.
var logBookImagePathsToDelete = await _imageRepo
.Where(x => x.Path.StartsWith("business_logbook"))
.Select(x => x.Path)
.ToListAsync();
foreach(var path in logBookImagePathsToDelete)
await DeleteImageAsync(path);
If you wanted to do it all in one action without creating any variables, you could do something similar to the following:
(await _imageRepo
.Where(x => x.Path.Contains("_logbook"))
.Select(x => x.Path)
.ToListAsync())
.ForEach(async path => await DeleteImageAsync(path));

LINQ to entity, sort the subset of returned values

var myView = await _context.foo
.Include(p => p.subFoos)
.SingleOrDefaultAsync(m => m.FooID == id);
This code returns a row from the database and the linked sub rows held in the collection subFoos, what I'd like to do is order the collection rather than have them displayed in table order. I've got a field OrderBy but I can't figure out how to use it.
var myView = await _context.foo
.Include(p => p.subFoos)
.OrderBy(x => x.OrderBy)
.SingleOrDefaultAsync(m => m.FooID == id);
This orders by the top level row, rather than the collection. How do I apply OrderBy to the collection.
Thanks
Couldn't get any of the above to work so just retrieved the data using
var myView = await _context.foo
.Include(p => p.subFoos)
.SingleOrDefaultAsync(m => m.FooID == id);
then ordered the myView.subFoos by converting them to a List
myView.subFoos = myView.subFoos.ToList().OrderBy(x => x.OrderBy).ToList();
Only one database call, works nicely.

Replacing empty values with a default value in a Linq to Entities query

In the Linq to Entities query below I need to place a default value in the x.Number in the returned value if the query returns 0 OfficeTelephone objects. I have tried
x.Number??"555-1212" but that throws an error.
from c in Contacts
.Where(a => a.LastName.Contains("ANDUS")).Take(10)
select new
{
Id = c.Id,
OfficeTelephone = c.Telephones.Where(a=>a.TelephoneType.Name.Contains("Office")).Select(x => new { x.AreaCode, x.Number, x.TelephoneType, x.Primary })
}
I've tried something like:
from c in Contacts
.Where(a => a.LastName.Contains("ANDUS")).Take(10)
select new
{
Id = c.Id,
OfficeTelephone = c.Telephones
.Where(a=>a.TelephoneType.Name.Contains("Office"))
.Select(x => new { x.AreaCode, x.Number, x.TelephoneType, x.Primary })
.DefaultIfEmpty()}
But I'm not sure how to push a default object into the DefaultIFEmpty()
Use DefaultIfEmpty and pass a default instance with those value which you would want by default i.e. if no rows are returned.
try it like this:
Contacts.Where(a=>a.LastName.Contains("ANDUS"))
.Take(10)
.Select(x => new
{
Id = x.Id,
OfficeTelephone = x.Telephones
.Where(a=> a.Telephone.Name.Contains("Office"))
.Select(b=> new Telephone
{
b.AreaCode,
b.Number,
b.TelephoneType,
b.Primary
})
.DefaultIfEmpty(new Telephone())
});
where I've assumed that typeof(x.Telephones) == typeof(List<Telephone>)

Linq select subquery

As the title states, I'm trying to perform a select subquery in Linq-To-SQL. Here's my situation:
I have a database view which returns the following fields:
SourceId
LicenseId
LicenseName
CharacteristicId
CharacteristicName
Now I want to be able to store this in a model of mine which has the following properties
Id
Name
Characteristics (this is List which has Id, Name and Icon => Icon is byte[])
Here's the query I wrote which doesn't work:
var licensesWithCharacteristics =
_vwAllLicensesWithAttributesAndSourceIdRepository.GetAll()
.Where(x => x.SourceID == sourceId)
.Select(a => new LicenseWithCharacteristicsModel()
{
LicenseId = a.LicenseId,
LicenseName = a.LicenseName
,CharacteristicList = _vwAllLicensesWithAttributesAndSourceIdRepository.GetAll()
.Where(x => x.LicenseId == a.LicenseId)
.Select(c => new CharacteristicModel { Id = c.CharacteristicID, Name = c.CharacteristicName, Icon = c.Icon })
.Distinct().ToList()
})
.Distinct().ToList();
How would you solve this? I'm trying to do this in one query to keep my performance up, but I'm kind of stuck.
Your sample query and models are not that coherent (where does Icon come from, Characteristics or CharacteristicList), but anyway.
I do this in two parts, you can of course regroup this in one query.
I enumerate the result after the grouping, you may try to do without enumerating (all in linq to sql, but not sure it will work).
var groupedResult =
_vwAllLicensesWithAttributesAndSourceIdRepository.GetAll()
.Where(x => x.SourceID == sourceId)
.GroupBy(m => new {m.LicenseId, m.LicenseName})
.ToList();
var results = groupedResult.Select(group => new LicenseWithCharacteristicsModel {
LicenseId = group.Key.LicenseId,
LicenseName = group.Key.LicenseName,
Characteristics = group.Select(m=> new CharacteristicModel {
Id = m.CharacteristicId,
Name = m.CharacteristicName
}).ToList()
});
in "single query"
_vwAllLicensesWithAttributesAndSourceIdRepository.GetAll()
.Where(x => x.SourceID == sourceId)
.GroupBy(m => new {m.LicenseId, m.LicenseName})
.Select(group =>
new LicenseWithCharacteristicsModel
{
LicenseId = group.Key.LicenseId,
LicenseName = group.Key.LicenseName,
Characteristics = group.Select(m =>
new CharacteristicModel
{
Id = m.CharacteristicId,
Name = m.CharacteristicName
}).ToList()
});

Resources