I am trying to query a one to many relationship but cannot figure out how to do this. The problem I have is that the ID of the field I want to filter by lives in the join table (not the main table)...
Its probably easier to illustrate rather than explain!!
The two classes I have are
public class DbUserClient
{
public virtual string UserId { get; set; }
public virtual int ClientId { get; set; }
public virtual DateTime AssignedOn { get; set; }
public virtual DateTime? ClearedOn { get; set; }
// navigation properties
public virtual DbUser User { get; set; }
public virtual DbClient Client { get; set; }
}
and
public class DbClient
{
public virtual int ClientId {get;set;}
public virtual string EntityName { get; set; }
public virtual bool Deleted { get; set; }
// navigation properties
public ICollection<DbUserClient> UserClients { get; set; }
}
In the program I have a repository that exposes the Clients i.e.
public ObservableCollection<DbClient> Clients
{
get { return context.Clients.Local; }
}
I am binding to this which is why I am keen on querying via the Client as this will refresh my "Local" collection. However I can't seem to figure out a way to include the UserClients as well as add the "where" clause.
I have tried something like
context.Clients.Include(c => c.UserClients.Where(uc => uc.UserId == "ME"));
But this results in the following exception
"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.
Parameter name: path"
This works but unfortunately will not update my "Local" collection
from c in context.Clients
from uc in c.UserClients
where uc.ClientId == uc.ClientId && uc.UserId == "ME"
select new { c.ClientId, c.EntityName, uc.AssignedOn };
Any suggestions on where I have gone wrong?
Cheers
Abs
EDIT I : looking at the SQL Profiler the above query generates the following SQL
SELECT
[Extent1].[ClientId] AS [ClientId],
[Extent1].[EntityName] AS [EntityName],
[Extent2].[AssignedOn] AS [AssignedOn]
FROM [dbo].[Client] AS [Extent1]
INNER JOIN [dbo].[UserClient] AS [Extent2] ON [Extent1].[ClientId] = [Extent2]. [ClientId]
WHERE ([Extent2].[ClientId] = [Extent2].[ClientId]) AND (N'ME' = [Extent2].[UserId])
This is pretty simple and more or less along the lines of what I would have written myself if I was handcrafting the SQL
However although the suggested expression below works and as you pointed out populates the Local cache
context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == userId))
.Select(c => new { DbClient = c, DbUser = c.UserClients.Where(uc => uc.UserId == userId).FirstOrDefault() }).ToList();
it produces the following SQL. This looks alot more complicated than it needs to be and I am assuming will have performance implications
exec sp_executesql N'SELECT
[Filter2].[ClientId] AS [ClientId],
[Filter2].[EntityName] AS [EntityName],
[Filter2].[Deleted] AS [Deleted],
[Limit1].[UserId] AS [UserId],
[Limit1].[ClientId] AS [ClientId1],
[Limit1].[AssignedOn] AS [AssignedOn],
[Limit1].[ClearedOn] AS [ClearedOn]
FROM (SELECT [Extent1].[ClientId] AS [ClientId], [Extent1].[EntityName] AS [EntityName], [Extent1].[Deleted] AS [Deleted]
FROM [dbo].[Client] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[UserClient] AS [Extent2]
WHERE ([Extent1].[ClientId] = [Extent2].[ClientId]) AND ([Extent2].[UserId] = #p__linq__0)
) ) AS [Filter2]
OUTER APPLY (SELECT TOP (1)
[Extent3].[UserId] AS [UserId],
[Extent3].[ClientId] AS [ClientId],
[Extent3].[AssignedOn] AS [AssignedOn],
[Extent3].[ClearedOn] AS [ClearedOn]
FROM [dbo].[UserClient] AS [Extent3]
WHERE ([Filter2].[ClientId] = [Extent3].[ClientId]) AND ([Extent3].[UserId] = #p__linq__1) ) AS [Limit1]',N'#p__linq__0 nvarchar(4000),#p__linq__1 nvarchar(4000)',#p__linq__0=N'ME',#p__linq__1=N'ME'
EDIT II : After playing around some more, I have found a solution that seems to fulfill my requirement. Looking at the SQL Profiler, I am happy with the generated SQL. This is similar to that of my orginal query.
exec sp_executesql N'SELECT
[Extent1].[ClientId] AS [ClientId],
[Extent1].[EntityName] AS [EntityName],
[Extent1].[Deleted] AS [Deleted],
[Extent2].[UserId] AS [UserId],
[Extent2].[ClientId] AS [ClientId1],
[Extent2].[AssignedOn] AS [AssignedOn],
[Extent2].[ClearedOn] AS [ClearedOn]
FROM [dbo].[Client] AS [Extent1]
INNER JOIN [dbo].[UserClient] AS [Extent2] ON [Extent1].[ClientId] = [Extent2].[ClientId]
WHERE [Extent2].[UserId] = #p__linq__0',N'#p__linq__0 nvarchar(4000)',#p__linq__0=N'ME'
I am assuming that there is no lazy loading involved here. If someone could confirm I would be grateful
context.Clients.Join
(
context.UserClients,
c => c.ClientId,
uc => uc.ClientId,
(user, usrclient) => new { DbClient = user, DbUserClient = usrclient }
).Where(uc => uc.DbUserClient.UserId == userId).Load();
You can load the clients which have at least one user with UserId = "ME":
var clients = context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.ToList();
This loads the correct clients but no user is included.
If you include the users...
var clients = context.Clients.Include(c => c.UserClients)
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.ToList();
... you'll get the correctly filtered clients but it will include all users, not only the user "ME".
In order to get the users filtered as well your last approach, the projection, is the best way:
var clientsWithUser = context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.Select(c => new
{
Client = c,
User = c.UserClients.Where(uc => uc.UserId == "ME").FirstOrDefault()
})
.ToList();
This should also update the Local collection because you are loading full entities (Client and User) in the anonymous object list.
Edit
The last query in your question is fine, although it's not really the EF way to write a Join manually when you have navigation properties. The SQL and query result is most likely identical to:
context.UserClients.Include(uc => uc.Client)
.Where(uc => uc.UserId == userId)
.Load();
The Include in this query should translate into the same INNER JOIN that your hand-written LINQ Join produces.
Related
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");
I've got two entities, Users and Friendships which look like:
public class User
{
public int UserId { get; set; }
(...)
}
public class Friendship
{
public int SenderId { get; set; }
public int ReceiverId { get; set; }
(...)
}
And I would like to create simple query which in SQL would look like:
SELECT * FROM Users as U
INNER JOIN Friendships as F ON U.UserId = F.ReceiverId OR U.UserId = F.SenderId
Where U.Nick != VARIABLE
In other words I would like to select all friends of the user.
And I can't accomplish that. I've found solution where one creates two separate join queries with union and it works - but it's not efficient to create such query to db.
Joins in LINQ are always equijoins. Basically you need multiple from clauses and a where clause:
var query = from u in db.Users
where u.Nick != variable
from f in db.Friendships
where u.UserId == f.ReceiveId || u.UserId == f.SenderId
select ...;
Now in LINQ to Objects there are probably more efficient ways of doing this - but I'd expect a SQL-based LINQ provider to generate a query which has a good enough execution plan. It may not actually create a JOIN in the SQL, but I'd expect it to be the same execution plan as the join you've shown.
Simply write:
from U in db.Users
from F in Friendships.Where(x => U.UserId == F.ReceiverId || U.UserId == F.SenderId)
where U.Nick != VARIABLE
select new {u, f};
I have a problem with a project. I’m trying to get a list of companies that are located in a specific country or city.
Table structure:
Company
CompanyID
CompanyName
etc…
CompanyAddressDetails (relation table)
Company_CompanyID
CorrespondingAddress_AddressID
CorrespondingAddress:
AddressID
StreetName
RegionID
etc…
Region
RegionID
RegionName
RegionRegionTypeID
RegionDetails (relation table)
RegionParent
RegionChild
So to find an address in example Stockholm (which has ID 1198 in the Region table), I would do:
var addresses = from c in db.CorrespondingAddress select c;
addresses = addresses.Where(s => s.RegionID.Equals(1198));
And to find a company in Stockholm I would do:
companyModel = from c in db.Company select c;
companyModel = companyModel.Where(s => s.CorrespondingAddress.Any(x => x.RegionID.Equals(1198)));
But now I want to take into account the RegionDetails table (which has a parent, and child, for example: 1 (Sweden) is parent, and 1198 (Stockholm) is child etc)
How can I do to find a company which is located in Sweden, but has the ID 1198 (Stockholm) in its address row?
In plain SQL I would maybe do something like:
SELECT CompanyName FROM Company
LEFT JOIN CompanyAddressDetails ON (Company.CompanyID = CompanyAddressDetails.Company_CompanyID)
LEFT JOIN CorrespondingAddress ON (CompanyAddressDetails.CorrespondingAddress_AddressID = CorrespondingAddress.AddressID)
LEFT JOIN Region ON (CorrespondingAddress.RegionID = Region.RegionID)
WHERE CorrespondingAddress IN (SELECT RegionChild FROM RegionDetails WHERE RegionParent = 1)
First off, the following code can be refactored into one line:
companyModel = from c in db.Company select c;
companyModel = companyModel.Where(s => s.CorrespondingAddress.Any(x => x.RegionID.Equals(1198)));
Can't you just nest another Any?
companyModel = db.Company.Where(s => s.CorrespondingAddress.Any(x => x.Region.Any(r => r.RegionDetails.Any(rd => rd.Parent == 1 && rd.Child == 1198)));
Edit
Given the following property of CorrespondingAddress:
public virtual Region Region { get; set; }
And assuming Region has a property RegionDetails (making Region -> RegionDetails one-to-one):
public virtual RegionDetails RegionDetails{ get; set; }
The following should work:
companyModel = db.Company.Where(s => s.CorrespondingAddress.Any(x => x.Region.RegionDetails.RegionParent == 1 && x.Region.RegionDetails.RegionChild == 1198)));
since there is no model for the details tables (many-to-many relationship tables), I solved it by using this method:
companyModel = companyModel.Where(s => s.CorrespondingAddress.Any(x => x.Region.RegionParent.Any(d => d.RegionID == region)));
Given the classes A and B where
class A
{
string Name;
Ilist<B> BList;
}
class B
{
string Name;
}
With FluentNH mapping, relationship is many-to-many which is HasManyToMany(x => x.B) for A. B has no reference to A. NH version is 2.1.2.4000.
What should be the linq query to select the collection where each row contains B.Name and count of A's containing that B? Result must be the List of anonymous type who has 2 fields: Name and Count. Result also should include all B's, hence it should be outer join.
My intend is to get the result with minimum round-trips to database, possibly in one go.
If you want to do it in Linq in one hit in code, you could do this...
var result = Session.Linq<A>()
.SelectMany(a => a.BList, (a, b) => new { b.Name, A = a.Id })
.ToList()
.GroupBy(x => x.Name)
.Select(x => new { Name = x.Key, Count = x.Count() })
.ToList();
NHibernate.Linq (2.1.2.4000) can't handle a GroupBy after a SelectMany it seems, so the first ToList pulls all the data into memory. This is inefficient -- a SQL count would be better.
Alternatively, you could add a lazy loaded collection to your B class that goes back to A. If you're using a many-to-many table in the middle, that should be easy.
public class B
{
public virtual string Name { get; set; }
public virtual IList<A> AList { get; private set; }
}
Your query simply becomes...
var result = Session.Linq<B>()
.Where(b => b.AList.Count > 0)
.Select(b => new { b.Name, b.AList.Count }
.ToList();
Which produces very efficient SQL from Linq (using a count) and gives the same result.
OK I'm banging my head against a wall with this one ;-)
Given tables in my database called Address, Customer and CustomerType, I want to display combined summary information about the customer so I create a query to join these two tables and retrieve a specified result.
var customers = (from c in tblCustomer.All()
join address in tblAddress.All() on c.Address equals address.AddressId
join type in tblCustomerType.All() on c.CustomerType equals type.CustomerTypeId
select new CustomerSummaryView
{
CustomerName = c.CustomerName,
CustomerType = type.Description,
Postcode = address.Postcode
});
return View(customers);
CustomerSummaryView is a simple POCO
public class CustomerSummaryView
{
public string Postcode { get; set; }
public string CustomerType { get; set; }
public string CustomerName { get; set; }
}
Now for some reason, this doesn't work, I get an IEnumerable list of CustomerSummaryView results, each record has a customer name and a postcode but the customer type field is always null.
I've recreated this problem several times with different database tables, and projected classes.
Anyone any ideas?
I can't repro this issue - here's a test I just tried:
[Fact]
public void Joined_Projection_Should_Return_All_Values() {
var qry = (from c in _db.Customers
join order in _db.Orders on c.CustomerID equals order.CustomerID
join details in _db.OrderDetails on order.OrderID equals details.OrderID
join products in _db.Products on details.ProductID equals products.ProductID
select new CustomerSummaryView
{
CustomerID = c.CustomerID,
OrderID = order.OrderID,
ProductName = products.ProductName
});
Assert.True(qry.Count() > 0);
foreach (var view in qry) {
Assert.False(String.IsNullOrEmpty(view.ProductName));
Assert.True(view.OrderID > 0);
Assert.False(String.IsNullOrEmpty(view.CustomerID));
}
}
This passed perfectly. I'm wondering if you're using a reserved word in there?
This post seems to be referring to a similar issue...
http://groups.google.com/group/subsonicproject/browse_thread/thread/2b569539b7f67a34?hl=en&pli=1
Yes, the reason Rob's example works is because his projection's property names match exactly, whereas John's original example has a difference between CustomerType and type.Description.
This shouldn't have been a problem, but it was - the Projection Mapper was looking for properties of the same name and wasn't mapping a value if it didn't find a match. Therefore, your projection objects' properties would be default values for its type if there wasn't an exact name match.
The good news is, I got the latest source today and built a new Subsonic.Core.dll and the behavior is now fixed.
So John's code above should work as expected.
I just downloaded the latest build from 3/21/2010, which is about 2 months after the last poster on this thread, and the problem still exists in the packaged binary. Bummer.
Here what I have to do:
var data =
(from m in Metric.All()
where m.ParentMetricId == parentId
select new
{
m.MetricName,
m.MetricId,
})
.ToList();
var treeData =
from d in data
select new TreeViewItem
{
Text = d.MetricName,
Value = d.MetricId.ToString(),
LoadOnDemand = true,
Enabled = true,
};
return new JsonResult { Data = treeData };
If I try to do the projection directly from the Subsonic query, the Text property ends up with the ID, and the Value property ends up with the Name. Very strange.