Hi good day I am new with Entity Framework. I just wanna to know if there is a way I could improve my implementation. Here are the codes.
public async Task<List<Record>> GetRecordsByBatchId(string batchId, string source)
{
List<string> idList = new List<string>();
//[1] Get all parent ID from table 1 with a filter of source and batchId
var parentIds= await _context.Set<FirstTable>()
.Where(a => a.IsActive
&& a.BatchId.Equals(batchId)
&& a.Source.Equals(source)).Select(b => b.ParentId).ToListAsync();
if (parentIds.Count() == 0)
{
return new List<Record>();
}
//[2] Query idNumber of each parentId from [1] to SecondTable
List<long> idNumber = await _context.Set<SecondTable>()
.Where(a => parentIds.Contains(a.Id))
.Select(b => b.IdNumber).ToListAsync();
//[3] Query Record/s that contains idNumber from previous query [2]. it is possible that 1 or
//more records has same idNumber
List<Risk> recordByIdNumber = await _context.Set<SecondTable>()
.Where(a => idNumber.Contains(a.IdNumber)).ToListAsync();
//[4] In this part I just want to group the records in [3] by Id number and sort each group
//by its endorsementNumber in descending order and return the record with highest endorsement
//number for each group
return (from record in recordByIdNumber
group record by record.IdNumber into g
orderby g.Key
select g.OrderByDescending(risk =>risk.EndorsementNumber).FirstOrDefault()).ToList();
}
}
The model for the FirstTable
public class FirstTable
{
public Guid? ParentId{ get; set; }
public string BatchId { get; set; }
public string Source { get; set; }
public bool IsActive { get; set; }
}
The model for the SecondTable
public class SecondTable
{
public Guid Id{ get; set; }
public int EndorsementNumber { get; set; }
public long IdNumber { get; set; }
}
Note: I just include the necessary properties in the model.
This approach is working as expected. I just wanna know if there is a possibility that these queries could be optimized that there is only 1 query for the SecondTable table.
Any help would be greatly appreciated, thanks in advance.
Yes, queries 1-3 can and should be combined. In order to do that you need, to have navigation properties in your model. It seems that there is one-to-many relationship between FirstTable and SecondTable. Let's use Customer and Order instead.
class Customer {
int CustomerId
string BatchId
ICollection<Order> Orders
}
class Order {
int OrderId
int CustomerId
Customer Customer
Risk Risk
}
in which case you just write third query as
List<Risk> = await _context.Orders.Where(o => o.Customer.BatchId == batchId)
.Select(o => o.Risk).ToListAsync();
Obviously, I am only guessing the structure and the relationship. But hopefully, this can get you started. For me Contains() is "code smell". There is a high chance that there will be large list out of your first query, and contains() will produce a huge IN clause in the database, that can easily crash the system
var parentIds = _context.Set<FirstTable>()
.Where(a => a.IsActive
&& a.BatchId.Equals(batchId)
&& a.Source.Equals(source)).Select(b => new { b.parentId });
var risks = await (from s in _context.Set<SecondTable>()
join p in parentIds on s.Id equals p.parentId
join r in _context.Set<SecondTable>() on s.IdNumber equals r.IdNumber
select r).GroupBy(r=>r.IdNumber)
.Select(r=> r.OrderByDescending(risk =>risk.EndorsementNumber).FirstOrDefault())
.ToArrayAsync();
return risks;
You can have 1 query instead of 3. It will perform better as the number of the rows from the first query grows.
EDIT: As #SvyatoslavDanyliv mentioned in the comments, group-take operations may not work depending on the version of the EF and the provider you use. You may need to separate the query and the group by operation like below :
var result = await (from s in _context.Set<SecondTable>()
join p in parentIds on s.Id equals p.parentId
join r in _context.Set<SecondTable>() on s.IdNumber equals r.IdNumber
select r).ToArrayAsync();
var risks = result.GroupBy(r=>r.IdNumber)
.Select(r=> r.OrderByDescending(
risk =>risk.EndorsementNumber).FirstOrDefault())
.ToArray();
return risks;
Related
So the equivalent of this query:
select * from car
left join parts ON car.Id = parts.carId
where parts.MemberId = 1
is this, in EntityFrameworkCore LINQ , using an IQueryable which has already selected car.Include(x => x.parts):
queryable = queryable.Where(x =>
x.parts.Select(y => y.MemberId).Contains(1);
But how can I convert the following SQL to LINQ, so that it includes rows from the left car table that have no respective MemberId entries in the parts table?
select * from car
left join parts ON car.Id = parts.CarId and parts.MemberId = 1
Models:
public class Car
{
public int Id { get; set; }
public virtual ICollection<Part> Parts { get; set; }
}
public class Parts
{
public int Id { get; set; }
public int CarId { get; set; }
public virtual Car { get; set; }
public int MemberId { get; set; }
}
A filtered Include does exactly what you want:
var cars = context.Cars
.Include(c => c.Parts.Where(p => p.MemberId == 1));
This doesn't generate the shorter join statement with a composite condition, but an outer join to a filtered subquery on Parts, to the same effect.
queryable = queryable
.Where(x => x.parts.Select(y => y.MemberId).Contains(1) || !x.parts.Any());
Try it like that:
queryable = queryable.Include(x => x.parts).Where(x =>
x.parts.Any(y => y.MemberId == 1).ToList();
How can I get the list of products for specific project without including project? Something similar to
select p.Id, p.Name
from product p
inner join ProjectProduct j on p.Id = j.ProductId and j.ProjectId = #Id
The closest solution that I have is
var products = await (from proj in context.Project select proj)
.Where(proj => proj.Id == 1)
.Select(proj => proj.Product)
.ToListAsync();
but this returns a List<List<Product>>
in addition I need to be able to add the order by and pagination to that
I know that I can do it by raw sql like this
var param1 = new SqlParameter("#Id", id);
var query = #"select * from product p
inner join ProjectProduct j
on p.Id = j.ProductId and j.ProjectId = #Id ";
var queryable = context.product.FromSqlRaw(query, param1).AsQueryable();
await HttpContext.AddPaginationToHeader(queryable);
var products = await queryable.OrderBy(x => x.Name).Paginate(paginationDTO).ToListAsync();
But I was hoping to do it by linq to entity
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public List<Product> Product { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public List<Project> Project { get; set; }
}
public class ProjectProduct
{
public int ProjectId { get; set; }
public int ProductId { get; set; }
}
Thanks to Harald Coppoolse's response on another post
and for those that have the same problem
here are the steps
1- we define our query as queryable
var queryable = context.Project
.Where(proj => proj.Id == id)
.SelectMany(p => p.Product).AsQueryable();
2- Now we can pass the query to SQL server like this
var products = await queryable.OrderBy(x =>
x.Name).Skip(...).Take(...).ToListAsync();
for Skip and Take we can create a generic Extension for IQueryable
and add a method to that with return of
return queryable
.Skip((paginationDTO.Page - 1) * paginationDTO.PageSize)
.Take(paginationDTO.PageSize);
and of course our paginationDTO is nothing more than a POCO class with 2 properties of page and pageSize
try this:
var products = await context.Project
.Where(proj => proj.Id == 1)
.Select( proj=>proj.Products)
.FirstOrDefaultAsync();
if you want to order product after this you can do this common way
products= products.OrderBy(p=>p.Name).ToList();
it can be put in one line but EF sometimes generates a very ridiculous Sql script, so better to do this separately.
I have 3 tables: Person, PersonFriend, PersonGroup.
Using LINQ, i want to join the 3 tables, filter using a dynamically generated where clause, and select custom columns with flattened rows (flattened one-to-many relationship table columns).
Pseudo-SQL design:
CREATE TABLE Person (int id, varchar socialclass, date createddate);
CREATE TABLE Person_Friend (int id, id personid references person.id, id friendpersonid references person.id, varchar friendtype);
CREATE TABLE Person_Group (int id, int memberid references person.id, varchar membershiplevel);
Entities:
public class Person
{
public int Id { get; set; }
public string SocialClass { get; set; }
public DateTime? CreatedDate { get; set; }
public ICollection<PersonFriend> Friend { get; set; }
public ICollection<PersonGroup> Group { get; set; }
}
public class PersonFriend
{
public int Id { get; set; }
public int PersonId { get; set; }
public int FriendPersonId { get; set; }
public string FriendType { get; set; }
}
public class PersonGroup
{
public int Id { get; set; }
public int MemberId { get; set; }
public string MembershipLevel { get; set; }
}
query syntax LINQ:
var queryResult = from person in _context.Person
join friend in _context.PersonFriend on person.Id equals friend.FriendPersonId
join group in _context.PersonGroup on person.Id equals group.MemberId
where (friend.PersonId == 1 && friend.FriendType == "type1") || (friend.PersonId == 3 && friend.FriendType == "type2") || ...
select new { person.Id, person.SocialClass, person.CreatedDate, friend.FriendPersonId, friend.FriendType, group.Id, group.MembershipLevel };
Notice the where clause; Given a list of { PersonId, FriendType } object, I want to build the where clause like above.
Since I could not figure building a dynamic where clause for a query syntax LINQ,
I tried converting it to the Method syntax LINQ statement so i can leverage the PredicateBuilder (http://www.albahari.com/nutshell/predicatebuilder.aspx) but I run into the problem during Selecting one-to-many things into a flattened object.
var methodResult = _context.Person
.Include(x => x.Friend)
.Include(x => x.Group)
.Select(person => new { person.Id, person.SocialClass, person.CreatedDate, person.friend.FriendPersonId, person.friend.FriendType, person.group.Id, person.group.MembershipLevel });
notice that the above Select is not possible because friend is a ICollection.
I also tried using the above query syntax LINQ statement without the where clause, making it return a object instead of an annonymous object, and then calling the method .Where() with the predicate builder. But the built expression runs into LINQ => Entity Framework SQL conversion error and executes the where in the application, not in DB.
var queryResultWithoutWhere = from person in _context.Person
join friend in _context.PersonFriend on person.Id equals friend.FriendPersonId
join group in _context.PersonGroup on person.Id equals group.MemberId
select new SelectedObject { PersonId = person.Id, SocialClass = person.SocialClass, CreatedDate = person.CreatedDate, FriendId = friend.FriendPersonId, FriendType = friend.FriendType, GroupId = group.Id, MembershipLevel = group.MembershipLevel };
var predicate = PredicateBuilder.New<SelectedObject>(false);
foreach (var searchObject in searchRequestObjects)
{
predicate.Or(p => p.FriendPersonId == searchObject.FriendPersonId && p.FriendType == searchObject.FriendType);
}
var result = queryResultWithoutWhere.Where(predicate).ToList();
I feel like I tried everything I could, and I cannot seem to generate this SQL. Last resort would be writing a raw SQL string and then executing it, but I really would like to get this working with Entity Framework.
How would I accomplish creating a dynamic where clause, select into a custom flattened object, and have entity framework generate the SQL?
You can use SelectMany to flatten the collections:
var methodResult = Persons
.Include(x => x.Friend)
.Include(x => x.Group)
.SelectMany(person =>
person.Friend.SelectMany(friend =>
person.Group.Select(group =>
new {
person.Id,
person.SocialClass,
person.CreatedDate,
friend.FriendPersonId,
friend.FriendType,
GroupId = group.Id,
group.MembershipLevel
}
)
)
);
I have the following two classes;
public class Order
{
public int Id {get;set;}
public List<Item> Items {get;set;}
public DateTime CreatedOn {get;set;}
}
public class Item
{
public int Code {get;set;}
public int SupplierId {get;set;}
public string Name {get;set;}
public decimal Price {get;set;}
}
I have a List of Orders, and each order contains a number of different items. I am wanting to filter the Order List, so that I can achieve the following;
Return all orders where at least 1 item in the Item List has SupplierId = 1
Don't return any orders if no items match SupplierId = 1
UPDATE
How can I expand the results as well so that I only return orders and Items that have SupplierId = 1
This is what I tried so far which now works. But how can I compact it further;
List<Order> OrderList = new List<Order>();
foreach(Order order in Order.Get(1))
{
Order tmpOrder = order;
tmpOrder.Items = order.Items.Where(x => x.SupplierId == 1).ToList();
if (tmpOrder.Items.Count > 0)
OrderList.Add(tmpOrder );
}
Return all orders where at least 1 item in the Item List has SupplierId = 1
Don't return any orders if no items match SupplierId = 1
IEnuemrable<Order> orders = //...
var supplierOrders = orders.where(o => o.Items.Any(i => i.SupplierId == 1))
.ToList();
Seems pretty straight forward. Unless you mean return ALL orders if ANY order has a supplierId of 1.
Update 1
How can I expand the results as well so that I only return orders and Items that have SupplierId = 1
EF currently (to my knowledge up to EF6) does not allow you to do this in a single query. It is still very easy and actually performant in consideration to the Cartesian Product problem EF faces (sometimes).
public class MyDbContext : DbContext
{
DbSet<Order> Order { get; set; }
DbSet<Item> Items { get; set; }
|
// each of these orders do not contain any
// items, we did not .Include() them.
var supplierOrders = db.Orders
.Where(o => o.Items
.Any(i => i.SupplierId == 1))
.ToList();
var orderIds = supplierOrders
.Select(so => so.Id)
.ToList();
var supplierItems = db.Items.
.Where(i => orderIds.Contain(i.SupplierId))
.ToList();
Because we're using EF Context, it automatically will wire up any items the contact caches locally to any associated item as long as you have the correct relationships setup in EF (which I would recommend doing).
In project I have this tables:
Product(id,catalogId, manufacturerId...)
Catalog
Manufacturer
Also Product model (id, name, catalogId, catalogTitle, manufacturerId, manufacturerName).
How can write in Linq this SQL query below if I want get Product item?
SELECT Product.Name, Product.CatalogId, Product.ManufacturerId, [Catalog].Name, Manufacturer.Name
FROM Product, [Catalog], Manufacturer
WHERE [Catalog].Id=Product.CatalogId AND Manufacturer.id=Product.ManufacturerId AND Product.Active=1
First, I'll answer your question.. then address your answer to comments. To answer your question, in Linq you would do the following:
from p in Product
join c in Catalog on c.Id equals p.CatalogId
join m in Manufacturer on m.Id equals p.ManufacturerId
where p.Active == 1
select new { Name = p.Name, CatalogId = p.CatalogId, ManufacturerId = p.ManufacturerId, CatalogName = c.Name, ManufacturerName = m.Name };
This will give you an anonymous object with the items you requested. If you need to use this elsewhere (and you're not using dynamic objects), I would suggest creating a view-model, and instantiating one of those in your select.
Example:
public class ProductInfoView
{
public string Name { get; set; }
public int CatalogId { get; set; }
public int ManufacturerId { get; set; }
public string CatalogName { get; set; }
public string ManufacturerName { get; set; }
}
from p in Product
join c in Catalog on c.Id equals p.CatalogId
join m in Manufacturer on m.Id equals p.ManufacturerId
where p.Active == 1
select new ProductInfoView() { Name = p.Name, CatalogId = p.CatalogId, ManufacturerId = p.ManufacturerId, CatalogName = c.Name, ManufacturerName = m.Name };
This will make referencing your query results a little less painful.
To answer your comment, you're doing a lot of joins if all you want is the product. Your criteria will only ensure three things
Your product's Active flag is 1
Your product has an existing Catalog entry
Your product has an existing Manufacturer entry
If #2 and #3 are superfluous and you don't necessarily need the names, you could simply do:
from p in Product
where p.Active == 1
select p
If Product is a CRUD model, you could potentially deep-load it to include Manufacturer/Catalog information, or use the aforementioned view-model.
Good luck!
To combine results from multiple tables without explicitly joins:
from p in Product
from c in Catalog
from m in Manufacturer
where c.Id == p.CatalogId && m.Id == p.ManufacturerId && p.Active == 1
select new
{
p.Name,
p.CatalogId,
p.ManufacturerId,
c.Name,
m.Name
};