I've got an ASP.NET MVC application using EF in C#. I can write and run my query correctly in SQL Server. In EF, I'm not sure how to accomplish the same thing.
Tables A, B and C. C references B which References A.
The query looks like this:
Select *
From C
Where C.bID in (Select B.bID
From B
Where B.aID = '<unique Key In A>')
The sub-query returns multiple B keys. Which then pass it through and look up the ID's in C. In short I'm looking up all data in C related to a key in A.
I just don't know how to put that into EF language. Or if it's even possible. The IN operator is what's throwing me on the conversion.
Example:
var exampleList = _context.C
.Where(l => l.bId in (_context.B
.Where(p => p.aId = keyInA)));
"in" doesn't work here. Obviously. After I wrote this post I made sure of it.
Note: A:B has a 1:Many relation. B:C has a 1 to many relation. All IDs and keys are GUIDs
Set up the navigation property between C & B, and between A & B. This is pretty much the whole point of using Entity Framework rather than just an alternative to ADO and SQL queries.
C contains a BId, so set up a B navigation property:
public class C
{
public int CId { get; set; }
[ForeignKey("B")]
public int BId { get; set; }
public virtual B B { get; set; }
}
B contains an AId, so similar:
public class B
{
public int BId { get; set; }
[ForeignKey("A")]
public int AId { get; set; }
public virtual A A { get; set; }
}
Now, to write your query:
var cs = context.Cs.Where(x => x.B.AId == id);
or
var cs = context.Cs.Where(x => x.B.A.AId == id); // can filter on other "A" fields this way.
I mean, to avoid the use of navigation properties, you may as well just write Sprocs and use ADO. It is possible in cases where you have unrelated tables or absolutely need to avoid a navigation property. Use a Join between context.Cs and context.Bs:
var cs = context.Cs.Join(context.Bs, c => c.BId, b => b.BId, (c, b) => new { C = c, AId = b.AId })
.Where(x => x.AId == aid)
.Select(x => x.C);
IMO seriously ugly working with joins in EF, but sometimes necessary. If you can use a navigation property I'd highly recommend it over using something like that regularly.
Related
I have an odd issue with AutoMapper (I'm using .NET core 3.1 and AutoMapper 10.1.1)
I'm doing a simple project to list and a simple projected count for total records:
var data = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.ToList();
var count = Db.Customers
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.Count();
The first line creates the expected SQL:
exec sp_executesql N'SELECT [c].[Code], [c].[Id], [c].[Name], [c].[Website], [s].Name
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
ORDER BY (SELECT 1)
OFFSET #__p_0 ROWS FETCH NEXT #__p_1 ROWS ONLY',N'#__p_0 int,#__p_1 int',#__p_0=0,#__p_1=25
The second line, the Count(). Seems to ignore the projection entirely:
SELECT COUNT(*)
FROM [Customers] AS [c]
The result of this is that any customer with a null StatusId will be excluded from the first query but included in the count in the second. Which breaks paging.
I would have thought that project should create something like:
SELECT COUNT(*)
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
Anyone know why the Count() is ignoring the ProjectTo<>?
Edit
Execution plan:
value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Domain.Customer]).Select(dtoCustomer
=> new CustomerViewModel() { Code = dtoCustomer.Code, Id = dtoCustomer.Id, Name = dtoCustomer.Name, StatusName =
dtoCustomer.Status.Name, Website = dtoCustomer.Website})
Edit 2021/02/19
Mappings plan:
EF entities -
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Code { get; private set; }
public string Website { get; private set; }
public CustomerStatus Status { get; private set; }
public Customer() { }
}
public class CustomerStatus
{
public Guid Id { get; private set; }
public string Name { get; private set; }
}
ViewModel -
public class CustomerViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Website { get; set; }
public string StatusName { get; set; }
}
Mapping -
CreateMap<Customer, CustomerViewModel>();
Edit 2021/02/20 - Manually Excluding Status
As pointed out in #atiyar answer you can manually exclude the status. This crosses me as a work around. My reasoning is this:
If you execute this query, as the very root query:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider)
You get:
exec sp_executesql N'SELECT TOP(#__p_0) [c].[Id], [c].[Name], [c0].[Name]
AS [StatusName]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]',N'#__p_0
int',#__p_0=5
This shows automapper understands and can see that there is a needed relationship between Status and Customer. But when you apply the count mechanism:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Count()
Suddenly, the understood relationship between Status and Customer is lost.
SELECT COUNT(*)
FROM [Customers] AS [c]
In my experience with Linq each query step modifies the previous step in a predicable way. I would have expected the count to build on the first command and include the count as part of that.
Interestingly, if you execute this:
_context.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Take(int.MaxValue).Count()
Automapper applies the relationship and the result is what I would have expected:
exec sp_executesql N'SELECT COUNT(*)
FROM (
SELECT TOP(#__p_0) [c].[Id], [c].[Name], [c0].[Name] AS [Name0], [c0].[Id]
AS [Id0]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]
) AS [t]',N'#__p_0 int',#__p_0=2147483647
Edit 2021/02/20 - Latest Version
Seems behaviour is the same in the latest version.
FYI: We have a scenario where records are imported on a regular basis from another application. We were hoping to use the inner join to exclude the records that don't have a matching record in another table. Then those records would be updated at a later point by the import process.
But from the application point of view it should always ignore those records hence the inner join and the status being mandatory. But we will have to manually exclude them (as per atiyar's solution) using the where to prevent paging from returning blown out page count numbers.
Edit 2021/02/20 - Further Digging
This does appear to be a design choice by the EF team and an optimisation. The assumption here is that if the relationship is non-null able. Then the join wont be included as a performance boost. The way around this is as suggested by #atiyar. Thanks for the help everyone #atiyar & #Lucian-Bargaoanu.
I have tested your code in .NET Core 3.1 with Entity Framework Core 3.1 and AutoMapper 10.1.1. And -
your first query generates a LEFT JOIN, not an INNER JOIN like you posted. So, the result from that query will not exclude any customer with a null StatusId. And, the generated SQL is same with ProjectTo<> and manual EF projection. I'd suggest to check your query and generated SQL again to make sure.
your second query generates the same SQL, the SQL you have posted, with ProjectTo<> and manual EF projection.
A solution for you :
If I understand correctly, you are trying to get -
a list of Customer, within the specified range, who has a related Status
the count of all such customers in your database.
Try the following -
Add a nullable foreign-key property in your Customer model -
public Guid? StatusId { get; set; }
This will help to simplify your queries and the SQL they generate.
To get your expected list, modify the first query as -
var viewModels = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.Where(p => p.StatusId != null)
.ProjectTo<CustomerViewModel>(_Mapper.ConfigurationProvider)
.ToList();
It will generate the following SQL -
exec sp_executesql N'SELECT [t].[Code], [t].[Id], [t].[Name], [s].[Name] AS [StatusName], [t].[Website]
FROM (
SELECT [c].[Id], [c].[Code], [c].[Name], [c].[StatusId], [c].[Website]
FROM [Customers] AS [c]
ORDER BY (SELECT 1)
OFFSET #__p_0 ROWS FETCH NEXT #__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [Statuses] AS [s] ON [t].[StatusId] = [s].[Id]
WHERE [t].[StatusId] IS NOT NULL',N'#__p_0 int,#__p_1 int',#__p_0=0,#__p_1=25
To get your expected count, modify the second query as -
var count = Db.Customers
.Where(p => p.StatusId != null)
.Count();
It will generate the following SQL -
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL
I am using a database-first approach with Entity Framework in .NET Core which ends up using this. Some tables are with a relationship in SQL Server which results joined in code.
I was about to get a DateTime on a joined table and want to give a string format, but to do it I should execute first the query so I could give a format to it like what was mentioned here. The question is, why did the joined table go null after the execution?
To explain my problem easily here's the code:
var a = (from p in db.Product
select s).ToList();
var b = (from p in a
select p.Brand.DateCreated.ToString("MMMM dd, yyyy")).ToList();
// Brand contains null after execution in "a"
var c = (from p in db.Product
select p.Brand.DateCreated.ToString("MMMM dd, yyyy")).ToList();
// Brand contains data but execution gives error due to DateTime.ToString()
In the meantime, This is my solution but I need to create another Model. Which will end up a lot of Model just for this case.
public class ProductBrand
{
public class Product { get; set; }
public class Brand { get; set; }
}
var a = (from s in db.Product
select new ProductBrand {
Product = s,
Brand = s.Brand,
}).ToList().Select(s => s.Brand.DateCreated.ToString("MMMM dd, yyyy")).ToList();
EDITTED:
Above problem was solved by just updating my Model. But problem related to above question still encounter. When saving new Product the Brand becomes null after saving
To explain my problem easily here's the code:
var p = new Product { Name = "temp", BrandId = 1 };
db.Product.Add(p);
db.SaveChanged();
return p.Brand.DateCreated.ToString("MMMM dd, yyyy"); // but Brand is null
In the meantime
var p = new Product { Name = "temp", BrandId = 1 };
db.Product.Add(p);
db.SaveChanged();
if(p.Brand == null) { // Here's my temporary solution, but how about for those tables with lot of joins
p.Brand = db.Brand.FirstOrDefault(s => s.Id == p.BrandId);
}
return p.Brand.DateCreated.ToString("MMMM dd, yyyy");
Did I miss something?
Given the following:
public class SomePoco {
public int IntValue { get; }
}
and
CREATE TABLE SomePocoStorage (IntValue INT NOT NULL)
and
INSERT SomePocoStorage VALUES (1), (274)
If I call
connection.Query<SomePoco>("SELECT * FROM SomePocoStorage")
does Dapper handle populating the IntValue field on the returned SomePoco instances?
Good question! It isn't a scenario I've targeted, but I'd be more than happy to take a look at what would be involved. Since we already do a lot of nasty reflection, this could still be viable. Probably better as a github issue, but I'll have a look.
Update - it does now (at the current time, via repo only - not deployed):
[Fact] // passes
public void GetOnlyProperties()
{
var obj = connection.QuerySingle<HazGetOnly>(
"select 42 as [Id], 'def' as [Name];");
obj.Id.IsEqualTo(42);
obj.Name.IsEqualTo("def");
}
class HazGetOnly
{
public int Id { get; }
public string Name { get; } = "abc";
}
No because there's no way for Dapper to set the value of the property if that property only has a getter.
I have two legacy SQL tables:
Contact
Id(uniqueidentifier, not null)
Foo
GlobalId(nvarchar(50, null)
EntityName(nvarchar(100, null)
The Foo.GlobalId column stores the ID from other tables in the DB, so to get at relevant data, we'd join tables like this:
select * from Foo
inner join Contact on Foo.EnitityName = 'contact'
and cast(Foo.Globalid as unqiqueidentifier) = Contact.Id
How do I express this relationship or cast the nvarchar to uniqueidentifier (or vice versa) using FluentAPI, or failing that, how can I perform the SQL cast() using LINQ?
public class Contact
{
public Guid Id {get;set;}
public virtual ICollection<Foo> Foos {get;set;}
}
public class ContactMap : EntityTypeConfiguration<Contact>
{
HasKey(t => t.Id);
HasMany(c => c.Foos)
.WithOptional(foo => foo.Contact)
/* ??? */;
}
public class Foo
{
public string GlobalId {get;set;}
public Contact Contact {get;set}
}
public class FooMap : EntityTypeConfiguration<Foo>
{
Property(t => t.PsmsGlobalId).HasMaxLength(50);
Property(t => t.PsmsEntityName).HasMaxLength(100);
}
Entity Framework currently does not support SQL type casts. Creating a SQL view which performed the cast worked well for me.
How do I search into my collection ??
Can't get it working... Don't I just have to do :
Contacts c = new Contacts();
if (c.Contact_name == "Test") {
MessageBox.Show("exists!");
}
Does not work :-)
public ObservableCollection<Contacts> contacts = new ObservableCollection<Contacts>();
class Contacts
{
public string Contact_id { get; set; }
public string Contact_name { get; set; }
}
You're setting c to a new instance of Contacts which does not have the Contact_name property set to anything...
If you're trying to search a collection for a specific contact, the easiest way would probably be to use the following Linq statement, which will return the first object in the collecting matching your condition, or null if no object is found
contacts.FirstOrDefault(p => p.Contact_name == "Test");
There's other Linq extensions that may be better suited for you depending on what you want too, such as .Exists() if you only want to know if an item exists or not
If you're not using Linq, the easiest way would be with a loop
foreach(var c in contacts)
{
if (c.Contact_name == "Test") {
MessageBox.Show("exists!");
}
}