Entity Framework Linq, Left Join and group with SUM and Count - c#

I need a little help in converting SQL to Linq. It's pretty straight forward in MySQL...
Table: customers
ID Name
1 Bill
2 John
Table: purchases
ID CustomerID CompletedTransaction
1 1 False
2 2 True
3 1 True
4 1 True
SELECT c.ID
c.Name,
COUNT(p.ID) AS TotalPurchases,
SUM(CASE WHEN p.CompletedTransaction = TRUE THEN 1 ELSE 0 END) AS TotalCompleted
FROM customers c
LEFT JOIN purchases p ON c.ID = p.CustomerID
GROUP BY c.ID
Expected Result:
1, Bill, 3, 2
2, John, 1, 1
I've seen a few examples on how to implement a left join in Linq but I'm not sure how to include a SUM and Count into this. I've seen examples in Linq where the fields returned are selected from the group keys. Does this mean that if I have more fields in the customers table such as address and other contact details which I'd like to return, I'd have to include them in the join to then be able to select them? Hope this makes sense. Appreciate any help or links that might point me in the right direction.
Thanks

var answer = (from c in db.customers
join p in db.purchases
on c.ID = p.CustomerID into subs
from sub in subs.DefaultIfEmpty()
group sub by new { c.ID, c.Name } into gr
select new {
gr.Key.ID,
gr.Key.Name,
Total = gr.Count(x => x != null),
CountCompleted = gr.Count(x => x != null && x.CompletedTransaction)
}).ToList();

Here's the sample
class Program
{
static void Main(string[] args)
{
List<Customers> customers = new List<Customers>();
customers.Add(new Customers() { ID = 1, Name = "Bill" });
customers.Add(new Customers() { ID = 2, Name = "John" });
List<Purchases> purchases = new List<Purchases>();
purchases.Add(new Purchases() { ID = 1, CustomerID = 1, CompletedTransaction = false });
purchases.Add(new Purchases() { ID = 2, CustomerID = 2, CompletedTransaction = true });
purchases.Add(new Purchases() { ID = 3, CustomerID = 1, CompletedTransaction = true });
purchases.Add(new Purchases() { ID = 4, CustomerID = 1, CompletedTransaction = true });
IEnumerable<JoinResult> results = from c in customers
join p in purchases
on c.ID equals p.CustomerID
group new { c, p } by new {p.CustomerID, c.Name} into r
select new JoinResult
{
CustomerID = r.Key.CustomerID,
CustomerName = r.Key.Name,
TotalPurchases = r.Count(),
TotalCompleteTransaction = r.Where(s=> s.p.CompletedTransaction).Count()
};
foreach(JoinResult r in results)
{
Console.WriteLine($"CustomerID : {r.CustomerID} | Name : {r.CustomerName} | TotalPurchases : {r.TotalPurchases} | TotalCompleteTransaction : {r.TotalCompleteTransaction}");
}
Console.ReadKey();
}
}
class Customers
{
public int ID { get; set; }
public string Name { get; set; }
}
class Purchases
{
public int ID { get; set; }
public int CustomerID { get; set; }
public bool CompletedTransaction { get; set; }
}
class JoinResult
{
public int CustomerID { get; set; }
public string CustomerName { get; set; }
public int TotalPurchases { get; set; }
public int TotalCompleteTransaction { get; set; }
}
Result

Related

How to use ORDER BY COUNT in LINQ and SELECT COUNT it?

I have a query and I don't know change it to LINQ
select distinct m.id,m.name, sch.id as schedule, COUNT(tk.id) as tiketSold
from movies m, schedules sch, tickets tk
where m.id = sch.movie_id and sch.id = tk.schedule_id
group by m.id,m.name, sch.id
order by COUNT(tk.id) desc
I'm trying:
var hotMovie = from m in _db.movies
from sch in _db.schedules
from tk in _db.tickets
where m.id == sch.movie_id && sch.id == tk.schedule_id
group m by m.id into g
orderby g.Count()
select new { Movie = g};
I do not have your database, so, I have created 3 classes like your table as I can anticipate. Then I have created 3 list like you table in the "TestMethod". In the linq query, I have joined the 3 list as you shown in sql query segment "where m.id = sch.movie_id and sch.id = tk.schedule_id" and then I perform the group by, order by an select. Here is my code, please try it and let me know it works or not.
public class movies
{
public int id { get; set; }
public string name { get; set; }
}
public class schedules
{
public int id { get; set; }
public int movie_id { get; set; }
}
public class tickets
{
public int id { get; set; }
public int schedule_id { get; set; }
}
void TestMethod()
{
//Add Movies to the list
List<movies> moviesItems = new List<movies>();
moviesItems.Add(new movies() { id = 1, name = "A" });
moviesItems.Add(new movies() { id = 2, name = "B" });
//Add Schedules to the list
List<schedules> schedulesItems = new List<schedules>();
schedulesItems.Add(new schedules() { id = 1, movie_id = 1 });
schedulesItems.Add(new schedules() { id = 2, movie_id = 2 });
schedulesItems.Add(new schedules() { id = 3, movie_id = 1 });
schedulesItems.Add(new schedules() { id = 4, movie_id = 2 });
//Add Tickets to the list
List<tickets> ticketsItems = new List<tickets>();
ticketsItems.Add(new tickets() { id = 1, schedule_id = 1 });
ticketsItems.Add(new tickets() { id = 2, schedule_id = 1 });
ticketsItems.Add(new tickets() { id = 3, schedule_id = 2 });
ticketsItems.Add(new tickets() { id = 4, schedule_id = 2 });
ticketsItems.Add(new tickets() { id = 5, schedule_id = 2 });
ticketsItems.Add(new tickets() { id = 6, schedule_id = 3 });
ticketsItems.Add(new tickets() { id = 7, schedule_id = 3 });
ticketsItems.Add(new tickets() { id = 8, schedule_id = 3 });
ticketsItems.Add(new tickets() { id = 9, schedule_id = 3 });
ticketsItems.Add(new tickets() { id = 10, schedule_id = 4 });
var query = from final in (from m in moviesItems
join sch in schedulesItems on m.id equals sch.movie_id
join tk in ticketsItems on sch.id equals tk.schedule_id
select new { movieID = m.id, movieName = m.name, schID = sch.id, tkID = tk.id })
group final by new { final.movieID, final.movieName, final.schID } into g
orderby g.Count() descending
select new { g.Key.movieID, g.Key.movieName, g.Key.schID, tiketSold = g.Count() };
}
This query is closest to your SQL but probably you need LEFT JOIN. Also it can be simplified using navigation properties if you provide model.
var hotMovie =
from m in _db.movies
join sch in _db.schedules on m.id equals sch.movie_id
join tk in _db.tickets on sch.id equals tk.schedule_id
group tk by new { movieID = m.id, movieName = m.name, scheduleId = sch.id } into g
orderby g.Sum(x => x.id != null ? 1 : 0) descending
select new
{
g.Key.movieID,
g.Key.movieName,
g.Key.scheduleId,
tiketSold = g.Sum(x => x.id != null ? 1 : 0)
};

LINQ get a record with max date from multiple joins

I have three tables: Courses, CourseLocations, CourseSchedules
Each Course can be held in one or more Locations (1-to-many)
Each Location can host one or more Schedules (1-to-many)
I need to get all the courses, unique names, that have a Schedules.Date> Today and show also the MAX value of the date contained in the table CourseSchedules
My current linq code is:
var courses = (from c in db.Courses
join cl in db.CourseLocations on c.CourseID equals cl.CourseID
join cs in db.CourseSchedules on cl.CourseLocationID equals cs.CourseLocationID
where c.CourseStatusID == 1 && c.DeleteDate == null && ((c.CourseCategoryID == 1 && cs.EndDate >= courseEndDateFilter) || (c.CourseCategoryID == 3))
select new
{
c.CourseID,
CourseName = c.Name,
CourseEndDate = cs.EndDate
}).Distinct().OrderBy(o => o.CourseCategoryID).ThenBy(o => o.CourseName);
Where courseEndDateFilter is a variable used to define the date to filter.
The problem with the above query is that I get all the courses duplicated and not only the one with the MAX value of cs.EndDate
Is there a way (efficient) to do it?
#Ehsan is correct. You need a group by and then get the max value of EndDate. Given the following models:
public class Course
{
public int CourseID { get; set; }
public string Name { get; set; }
public int CourseStatusID { get; set; }
public int CourseCategoryID { get; set; }
public DateTime? DeleteDate { get; set; }
}
public class CourseLocation
{
public int CourseLocationID { get; set; }
public int CourseID { get; set; }
}
public class CourseSchedules
{
public int CourseLocationID { get; set; }
public DateTime EndDate { get; set; }
}
I created the following in memory objects:
var courses = new List<Course>
{
new Course { CourseID = 1, Name = "Test1", CourseCategoryID = 1, CourseStatusID = 1, DeleteDate = null },
new Course { CourseID = 2, Name = "Test2", CourseCategoryID = 1, CourseStatusID = 1, DeleteDate = null },
new Course { CourseID = 3, Name = "Test3", CourseCategoryID = 3, CourseStatusID = 1, DeleteDate = null }
};
var courseLocations = new List<CourseLocation>
{
new CourseLocation{ CourseID = 1, CourseLocationID = 1 },
new CourseLocation{ CourseID = 2, CourseLocationID = 1 },
new CourseLocation{ CourseID = 3, CourseLocationID = 1 },
new CourseLocation{ CourseID = 1, CourseLocationID = 2 },
new CourseLocation{ CourseID = 2, CourseLocationID = 2 },
new CourseLocation{ CourseID = 3, CourseLocationID = 2 }
};
var courseSchedules = new List<CourseSchedules>
{
new CourseSchedules { CourseLocationID = 1, EndDate = DateTime.Now.AddDays(10) },
new CourseSchedules { CourseLocationID = 1, EndDate = DateTime.Now.AddYears(1) }
};
Then the query would be the following to get Max EndDate:
var result = (from c in courses
join cl in courseLocations on c.CourseID equals cl.CourseID
join cs in courseSchedules on cl.CourseLocationID equals cs.CourseLocationID
where c.CourseStatusID == 1 && c.DeleteDate == null &&
(c.CourseCategoryID == 1 && cs.EndDate >= DateTime.Now || c.CourseCategoryID == 3)
select new
{
c.CourseID,
CourseName = c.Name,
CourseEndDate = cs.EndDate,
c.CourseCategoryID
})
.GroupBy(arg => new
{
arg.CourseID,
arg.CourseName,
arg.CourseCategoryID
})
.Select(grouping => new
{
grouping.Key.CourseID,
grouping.Key.CourseName,
CourseEndDate = grouping.Max(arg => arg.CourseEndDate),
grouping.Key.CourseCategoryID
})
.OrderBy(o => o.CourseCategoryID)
.ThenBy(o => o.CourseName);
I'm not sure this will work since it's not like I can actually compile it.
The problem I see right away is that you're filter by a few things at the base, some of which don't include the locations but you want the date from the locations anyway...
var courses = (from c in db.Courses
join cl in db.CourseLocations on c.CourseID equals cl.CourseID
join cs in db.CourseSchedules on cl.CourseLocationID equals cs.CourseLocationID
where c.CourseStatusID == 1 && c.DeleteDate == null && (c.CourseCategoryID == 3 ||
db.CourseLocations.Any(cl => cl.CourseID equals c.CourseID &&
db.CourseSchedules.Any(cs => cs.CourseLocationID equals cl.CourseLocationID &&
((c.CourseCategoryID == 1 && cs.EndDate >= courseEndDateFilter))
)
))
select new
{
c.CourseID,
CourseName = c.Name,
CourseEndDate = db.CourseSchedules.Where(cs => db.CourseLocations.Any(cl => cl.CourseID equals c.CourseID && cs.CourseLocationID equals cl.CourseLocationID)).Max(cs => cs.EndDate),
c.CourseCategoryID
});

Error null reference in left join

When i comment dishes.Add(new Dishes { DishID = 8, DishName = "Name", DishTypeID = 2, IngredientID = 2 }); i get in ll one item Amount="1 cup" DishID=1 Ingridient="egg" Name ="Soup". When uncomment that line raising error, null reference exception in b.IngredientTypeID. The main question how to get in ll two item's:
1) Amount="1 cup" DishID=1 Ingridient="egg" Name ="Soup"
2) Amount=null DishID=2 Ingridient=null Name =null
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
List<Dishes> dishes = new List<Dishes>();
List<Ingredients> ingredients = new List<Ingredients>();
List<Amount> amount = new List<Amount>();
List<Ingredient> ingredient = new List<Ingredient>();
dishes.Add(new Dishes { DishID = 1, DishName = "Soup", DishTypeID = 1, IngredientID = 1 });
//dishes.Add(new Dishes { DishID = 8, DishName = "Name", DishTypeID = 2, IngredientID = 2 });
ingredients.Add(new Ingredients { AmountID = 2, IngredientID = 1, IngredientTypeID = 1, IngredientUniqID = 1 });
amount.Add(new Amount { AmountID = 2, AmountName = "1 cup" });
ingredient.Add(new Ingredient { IngredientID = 1, IngredientName = "egg" });
var test = from dish in dishes
join ing in ingredients on dish.IngredientID equals ing.IngredientID into result
from b in result.DefaultIfEmpty()
join i in ingredient on b.IngredientTypeID equals i.IngredientID into r
from c in r.DefaultIfEmpty()
join am in amount on b.AmountID equals am.AmountID into s
from t in s.DefaultIfEmpty()
select new DisplayRecipe { Name = dish.DishName, Amount = t.AmountName, Ingredient = c.IngredientName, DishID = dish.DishID };
List<DisplayRecipe> ll = test.ToList();
}
}
public partial class Dishes
{
public int DishID { get; set; }
public string DishName { get; set; }
public Nullable<int> DishTypeID { get; set; }
public Nullable<int> IngredientID { get; set; }
}
public partial class Ingredients
{
public int IngredientID { get; set; }
public Nullable<int> AmountID { get; set; }
public Nullable<int> IngredientTypeID { get; set; }
public int IngredientUniqID { get; set; }
}
public partial class Amount
{
public int AmountID { get; set; }
public string AmountName { get; set; }
}
public partial class Ingredient
{
public int IngredientID { get; set; }
public string IngredientName { get; set; }
}
public class DisplayRecipe
{
public string Name { get; set; }
public string Ingredient { get; set; }
public string Amount { get; set; }
public int DishID { get; set; }
}
}
The problem is that any of the b, c, t variables can be null due to DefaultIfEmpty and you need to account for that in any member access, including join conditions.
If you are using C#6 (VS2015), you can use ?. operator like this
var test = from dish in dishes
join ing in ingredients on dish.IngredientID equals ing.IngredientID into result
from b in result.DefaultIfEmpty()
join i in ingredient on b?.IngredientTypeID equals i.IngredientID into r
from c in r.DefaultIfEmpty()
join am in amount on b?.AmountID equals am.AmountID into s
from t in s.DefaultIfEmpty()
select new DisplayRecipe { Name = dish.DishName, Amount = t?.AmountName, Ingredient = c?.IngredientName, DishID = dish.DishID };
while in pre C#6:
var test = from dish in dishes
join ing in ingredients on dish.IngredientID equals ing.IngredientID into result
from b in result.DefaultIfEmpty()
join i in ingredient on b != null ? b.IngredientTypeID : null equals i.IngredientID into r
from c in r.DefaultIfEmpty()
join am in amount on b != null ? b.AmountID : null equals am.AmountID into s
from t in s.DefaultIfEmpty()
select new DisplayRecipe { Name = dish.DishName, Amount = t != null ? t.AmountName : null, Ingredient = c != null ? c.IngredientName : null, DishID = dish.DishID };
Problem is you added this line:
dishes.Add(new Dishes { DishID = 8, DishName = "Name", DishTypeID = 2, IngredientID = 2 });
But did not also add the other lines that are dependent on your join (example):
ingredients.Add(new Ingredients { AmountID = 2, IngredientID = 2, IngredientTypeID = 1, IngredientUniqID = 1 });
ingredient.Add(new Ingredient { IngredientID = 2, IngredientName = "ham" });
So when your program tries to find an ingredientID of 2 because that has been added to dishes it does not find one and produces an error.
Sample of code that works:
dishes.Add(new Dishes { DishID = 1, DishName = "Soup", DishTypeID = 1, IngredientID = 1 });
dishes.Add(new Dishes { DishID = 8, DishName = "Name", DishTypeID = 2, IngredientID = 2 });
ingredients.Add(new Ingredients { AmountID = 2, IngredientID = 1, IngredientTypeID = 1, IngredientUniqID = 1 });
ingredients.Add(new Ingredients { AmountID = 2, IngredientID = 2, IngredientTypeID = 1, IngredientUniqID = 1 });
amount.Add(new Amount { AmountID = 2, AmountName = "1 cup" });
ingredient.Add(new Ingredient { IngredientID = 1, IngredientName = "egg" });
ingredient.Add(new Ingredient { IngredientID = 2, IngredientName = "ham" });

Two tables deep left outer join

I have the following query and I am getting a null reference exception:
(from cec in _myContext.table1s
join ceclrp in _myContext.table2s on cec.table1ID equals ceclrp.table1ID
join lrp in _myContext.table3s on ceclrp.table3ID equals lrp.table3ID
join cecs in _myContext.table4s on cec.table1ID equals cecs.table1ID into cecsGroup
from ecService in cecsGroup.DefaultIfEmpty()
join cecse in _myContext.table5s on ecService.table4ID equals cecse.table4ID into cecseGroup
from ecServiceEntitlement in cecseGroup.DefaultIfEmpty()
where cec.ClientKey == clientKey
select new
{
table1 = cec,
table2 = ceclrp,
table3 = lrp,
table4 = ecService,
table5 = ecServiceEntitlement,
}).AsNoTracking();
Table 1, 2, and 3 records are required tables. However table 4 record is optional. If there is a table 4 record, then there can be an optional table 5 record.
However, I am getting a null reference exception when there is no table 4 or table 5 record.
I have looked at other questions similar to mine on StackOverflow but I can't figure out what is different from my query and the solutions posted.
Can someone please help me figure out why I am getting the null reference exception and what I need to do differently?
According to MSDN,
The default value for reference and nullable types is null.
For more than one LEFT JOIN, we can handle the null value by passing defaultValue for the DefaultIfEmpty() extension method. The following sample code works when I pass the default Constructor new Driver(), it throws exception if the defaultValue is removed.
Here I have created three models (Dealer, Model and Customer) with matching records and two models (Driver, DriverAddress) with optional records. The same as OP scenario.
So, passing the default value for the first LEFT JOIN may solve your problem.
public class Dealer
{
public int DealerId { get; set; }
public string Name { get; set; }
}
public class Model
{
public int ModelId { get; set; }
public string Name { get; set; }
public Dealer Dealer { get; set; }
}
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Model Model { get; set; }
}
public class Driver
{
public int DriverId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Customer Customer { get; set; }
}
public class DriverAddress
{
public int DriverAddressId { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string Zip { get; set; }
public Driver Driver { get; set; }
}
class Program
{
static void Main(string[] args)
{
var honda = new Dealer { DealerId = 1, Name = "Honda" };
var ford = new Dealer { DealerId = 2, Name = "Ford" };
var toyoto = new Dealer { DealerId = 3, Name = "Toyoto" };
var volkswagen = new Dealer { DealerId = 4, Name = "Volkswagen" };
var chevrolet = new Dealer { DealerId = 5, Name = "Chevrolet" };
var civic = new Model { ModelId = 1, Name = "Civic", Dealer = honda };
var fiesta = new Model { ModelId = 2, Name = "Fiesta", Dealer = ford };
var corolla = new Model { ModelId = 3, Name = "Corolla", Dealer = toyoto };
var passat = new Model { ModelId = 4, Name = "Passat", Dealer = volkswagen };
var cruze = new Model { ModelId = 5, Name = "Cruze", Dealer = chevrolet };
var magnus = new Customer { CustomerId = 1, FirstName = "Magnus", LastName = "Hedlund", Model = civic };
var terry = new Customer { CustomerId = 2, FirstName = "Terry", LastName = "Adams", Model = fiesta };
var charlotte = new Customer { CustomerId = 3, FirstName = "Charlotte", LastName = "Weiss", Model = corolla };
var john = new Customer { CustomerId = 4, FirstName = "John", LastName = "Miller", Model = passat };
var arlene = new Customer { CustomerId = 5, FirstName = "Arlene", LastName = "Huff", Model = cruze };
var driver1 = new Driver { DriverId = 1, FirstName = "Fadi", LastName = "Fakhouri", Customer = magnus };
var driver2 = new Driver { DriverId = 2, FirstName = "Hanying", LastName = "Feng", Customer = terry };
var driver3 = new Driver { DriverId = 3, FirstName = "Cesar", LastName = "Garcia", Customer = charlotte };
var driver4 = new Driver { DriverId = 4, FirstName = "Lint", LastName = "Tucker", Customer = magnus };
var driver5 = new Driver { DriverId = 5, FirstName = "Robert", LastName = "Thomas", Customer = arlene };
var driver6 = new Driver { DriverId = 6, FirstName = "David", LastName = "Adams", Customer = charlotte };
var driver1Address = new DriverAddress { DriverAddressId = 1, AddressLine1 = "Main St", City = "Minnehaha", Zip = "57105", Driver = driver1 };
var driver2Address = new DriverAddress { DriverAddressId = 2, AddressLine1 = "State St", City = "Los Angeles", Zip = "90034", Driver = driver2 };
var driver3Address = new DriverAddress { DriverAddressId = 3, AddressLine1 = "Ralph St", City = "Winnebago", Zip = "61109", Driver = driver4 };
List<Dealer> lstDealers = new List<Dealer> { honda, ford, toyoto, volkswagen, chevrolet };
List<Model> lstModels = new List<Model> { civic, fiesta, corolla, passat, cruze };
List<Customer> lstCustomers = new List<Customer> { magnus, terry, charlotte, john, arlene };
List<Driver> lstDrivers = new List<Driver> { driver1, driver2, driver3, driver4, driver5, driver6 };
List<DriverAddress> lstDriverAddress = new List<DriverAddress> { driver1Address, driver2Address, driver3Address };
var result = from dealer in lstDealers
join model in lstModels on dealer.DealerId equals model.Dealer.DealerId
join customer in lstCustomers on model.ModelId equals customer.Model.ModelId
join driver in lstDrivers on customer.CustomerId equals driver.Customer.CustomerId into customerDriverGroup
from customerDriver in customerDriverGroup.DefaultIfEmpty(new Driver()) //defaultValue the empty constructor passed here
join address in lstDriverAddress on customerDriver.DriverId equals address.Driver.DriverId into driverAddressGroup
from driverAddress in driverAddressGroup.DefaultIfEmpty()
select new
{
Dealer = dealer,
Model = model,
Customer = customer,
Driver = customerDriver,
DriverAddress = driverAddress
};
foreach (var v in result)
{
Console.WriteLine("{0,-15}{1,-15}{2,-15}{3,-15}{4}", v.Dealer.Name + ":",
v.Model.Name + ":", v.Customer.FirstName + ":", v.Driver == null ? String.Empty : v.Driver.FirstName
+ ":", v.DriverAddress == null ? string.Empty : v.DriverAddress.City);
}
Console.Read();
}
}
You should check if your select new {... } values are null
Like:
select new
{
table1 = cec,
table2 = ceclrp,
table3 = lrp,
table4 = (ecService == null ? string.Empty : ecService),
table5 = (ecServiceEntitlement == null ? string.Empty : ecServiceEntitlement),
}).AsNoTracking();
This might be the problem.
join cecse in _myContext.table5s on ecService.table4ID equals cecse.table4ID into cecseGroup
When ecService == null, ecService .table4ID will cause a Null reference exception
Can you try this instead.
join cecse in _myContext.table5s on (ecService == null? 0 : ecService.table4ID) equals cecse.table4ID into cecseGroup
you can change 0 to any integer value which is not used in _myContext.table4s.table4ID. So your query would look like this.
(from cec in _myContext.table1s
join ceclrp in _myContext.table2s on cec.table1ID equals ceclrp.table1ID
join lrp in _myContext.table3s on ceclrp.table3ID equals lrp.table3ID
join cecs in _myContext.table4s on cec.table1ID equals cecs.table1ID into cecsGroup
from ecService in cecsGroup.DefaultIfEmpty()
join cecse in _myContext.table5s on (ecService == null? 0 : ecService.table4ID) equals cecse.table4ID into cecseGroup
from ecServiceEntitlement in cecseGroup.DefaultIfEmpty()
where cec.ClientKey == clientKey
select new
{
table1 = cec,
table2 = ceclrp,
table3 = lrp,
table4 = ecService,
table5 = ecServiceEntitlement,
}).AsNoTracking();
I would just expand the cecse variable into a left outer join too. Like how we would have done it in raw SQL too.
(from cec in _myContext.table1s
join ceclrp in _myContext.table2s on cec.table1ID equals ceclrp.table1ID
join lrp in _myContext.table3s on ceclrp.table3ID equals lrp.table3ID
join cecs in _myContext.table4s on cec.table1ID equals cecs.table1ID into cecsGroup
from ecService in cecsGroup.DefaultIfEmpty()
from cecse in _myContext.table5s.DefaultIfEmpty() // <---- here
from ecServiceEntitlement in cecseGroup.DefaultIfEmpty()
where cec.ClientKey == clientKey
&& ecService.table4ID equals cecse.table4ID // <---- here
select new
{
table1 = cec,
table2 = ceclrp,
table3 = lrp,
table4 = ecService,
table5 = ecServiceEntitlement,
}).AsNoTracking();

Linq distinct record containing keywords

I need to return a distinct list of records based on a car keywords search like: "Alfa 147"
The problem is that, as I have 3 "Alfa" cars, it returns 1 + 3 records (it seems 1 for the Alfa and 147 result, and 3 for the Alfa result)
EDIT:
The SQL-Server Query look something like this:
SELECT DISTINCT c.Id, c.Name /*, COUNT(Number of Ads in the KeywordAdCategories table with those 2 keywords) */
FROM Categories AS c
INNER JOIN KeywordAdCategories AS kac ON kac.Category_Id = c.Id
INNER JOIN KeywordAdCategories AS kac1 ON kac.Ad_Id = kac1.Ad_Id AND kac1.Keyword_Id = (SELECT Id FROM Keywords WHERE Name = 'ALFA')
INNER JOIN KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id AND kac2.Keyword_Id = (SELECT Id FROM Keywords WHERE Name = '147')
My LINQ query is:
var query = from k in keywordQuery where splitKeywords.Contains(k.Name)
join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
join c in categoryQuery on kac.Category_Id equals c.Id
join a in adQuery on kac.Ad_Id equals a.Id
select new CategoryListByKeywordsDetailDto
{
Id = c.Id,
Name = c.Name,
SearchCount = keywordAdCategoryQuery.Where(s => s.Category_Id == c.Id).Where(s => s.Keyword_Id == k.Id).Distinct().Count(),
ListController = c.ListController,
ListAction = c.ListAction
};
var searchResults = new CategoryListByBeywordsListDto();
searchResults.CategoryListByKeywordsDetails = query.Distinct().ToList();
The entities are:
public class Keyword
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
}
// Keyword Sample Data:
// 1356 ALFA
// 1357 ROMEO
// 1358 145
// 1373 147
public class Category
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
}
// Category Sample Data
// 1 NULL 1 Carros
// 2 NULL 1 Motos
// 3 NULL 2 Oficinas
// 4 NULL 2 Stands
// 5 NULL 1 Comerciais
// 8 NULL 1 Barcos
// 9 NULL 1 Máquinas
// 10 NULL 1 Caravanas e Autocaravanas
// 11 NULL 1 Peças e Acessórios
// 12 1 1 Citadino
// 13 1 1 Utilitário
// 14 1 1 Monovolume
public class KeywordAdCategory
{
[Key]
[Column("Keyword_Id", Order = 0)]
public int Keyword_Id { get; set; }
[Key]
[Column("Ad_Id", Order = 1)]
public int Ad_Id { get; set; }
[Key]
[Column("Category_Id", Order = 2)]
public int Category_Id { get; set; }
}
// KeywordAdCategory Sample Data
// 1356 1017 1
// 1356 1018 1
// 1356 1019 1
// 1357 1017 1
// 1357 1018 1
// 1357 1019 1
// 1358 1017 1
// 1373 1019 1
public class Ad
{
// Primary properties
public int Id { get; set; }
public string Title { get; set; }
public string TitleStandard { get; set; }
public string Version { get; set; }
public int Year { get; set; }
public decimal Price { get; set; }
// Navigation properties
public Member Member { get; set; }
public Category Category { get; set; }
public IList<Feature> Features { get; set; }
public IList<Picture> Pictures { get; set; }
public IList<Operation> Operations { get; set; }
}
public class AdCar : Ad
{
public int Kms { get; set; }
public Make Make { get; set; }
public Model Model { get; set; }
public Fuel Fuel { get; set; }
public Color Color { get; set; }
}
// AdCar Sample Data
// 1017 Alfa Romeo 145 1.6TDI 2013 ALFA ROMEO 145 1.6TDI 2013 12 2 1.6TDI 1000 1 2013 1 20000,0000 2052 AdCar
// 1018 Alfa Romeo 146 1.6TDI 2013 ALFA ROMEO 146 1.6TDI 2013 12 2 5 1.6TDI 1000 2 2013 1 20000,0000 2052 AdCar
// 1019 Alfa Romeo 147 1.6TDI 2013 ALFA ROMEO 147 1.6TDI 2013 12 2 6 1.6TDI 1000 3 2013 1 20000,0000 2052 AdCar
The result I expect for the search of "ALFA" is "Cars: 3" and for the search of "ALFA 147" is "Cars: 1" and actually the result I get is "Cars: 1 \n Cars: 3"
The kac is not filtering words... so this joins of kac, kac1 and kac2 will return 3 lines, cause this is the numbers of keywords for this ad
You should remove it..
Try this:
SELECT DISTINCT
c.Id, c.Name /*, COUNT(Number of Ads in the KeywordAdCategories table with those 2 keywords) */
FROM
Categories AS c
INNER JOIN
KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id
FROM Keywords
WHERE Name = 'ALFA')
AND kac1.Category_Id = c.Id
INNER JOIN
KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id
AND kac2.Keyword_Id = (SELECT Id
FROM Keywords
WHERE Name = '147')
AND kac2.Category_Id = c.Id
I did a test...
Setting the ambient as
declare #Keywords table(id int,name varchar(max))
insert into #Keywords(id,name)
values (1356,'ALFA')
,(1357,'ROMEO')
,(1358,'145')
,(1373,'147')
declare #Categories table(id int, name varchar(max))
insert into #Categories(id,name)
values (1,'Carros')
,(2,'Motos')
declare #KeywordAdCategories table(Keyword_Id int, ad_Id int,Category_Id int)
insert into #KeywordAdCategories (Keyword_Id , ad_Id,Category_Id)
values (1356, 1017,1)
,(1356, 1018,1)
,(1356, 1019,1)
,(1357, 1017,1)
,(1357, 1018,1)
,(1357, 1019,1)
,(1358, 1017,1)
,(1373, 1019,1)
I run these two queries:
--query 1
SELECT
c.Id, c.Name,COUNT(*) as [count]
FROM
#Categories AS c
INNER JOIN
#KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id
FROM #Keywords
WHERE Name = 'ALFA')
AND kac1.Category_Id = c.Id
GROUP BY
c.Id, c.Name
I get this result set:
Id Name count
----------- ---------- -----------
1 Carros 3
and the second query for two words...
--query 2
SELECT
c.Id, c.Name,COUNT(*) as [count]
FROM
#Categories AS c
INNER JOIN
#KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id
FROM #Keywords
WHERE Name = 'ALFA')
AND kac1.Category_Id = c.Id
INNER JOIN
#KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id
AND kac2.Keyword_Id = (SELECT Id
FROM #Keywords
WHERE Name = '147')
AND kac2.Category_Id = c.Id
GROUP BY
c.Id, c.Name
Result set is:
Id Name count
----------- ---------- -----------
1 Carros 1
Is this what you want?
You can use the Distinct() method.
var query = ...
var query = query.Distinct();
See This code returns distinct values. However, what I want is to return a strongly typed collection as opposed to an anonymous type for more details.
Split the query string into an array and iterate through querying the database for each keyword and joining the result sets using unions. The resultant set will be every distinct record that matches any of the given keywords.
Maybe this is close? At least the subqueries open it up a little for you to work with.
var query =
from c in categoryQuery
let keywords =
(
from k in keywordQuery where splitKeywords.Contains(k.Name)
join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
where kac.Category_Id == c.Id
join a in adQuery on kac.Ad_Id equals a.Id
select k.Id
).Distinct()
where keywords.Any()
select new CategoryListByKeywordsDetailDto
{
Id = c.Id,
Name = c.Name,
SearchCount =
(
from kac in keywordAdCategoryQuery
where kac.Category_Id == c.Id
join kId in keywords on kac.Keyword_Id equals kId
select kac.Id
).Distinct().Count(),
ListController = c.ListController,
ListAction = c.ListAction
};
One of the beautiful features of linq is that you can build up complicated queries in smaller and simpler steps and let linq figure out how to join them all together.
The following is one way to get this information. I'm not sure whether this is the best and you would need to check it performs well when multiple keywords are selected.
Assuming keywords is defined something like
var keywords = "Alfa 147";
var splitKeywords = keywords.Split(new char[] {' '});
Stage 1
Get a list of keywords grouped by Ad and Category and
var subQuery = (from kac in keywordAdCategoryQuery
join k in keywordQuery on kac.Keyword_Id equals k.Id
select new
{
kac.Ad_Id,
kac.Category_Id,
KeyWord = k.Name,
});
var grouped = (from r in subQuery
group r by new { r.Ad_Id, r.Category_Id} into results
select new
{
results.Key.Ad_Id ,
results.Key.Category_Id ,
keywords = (from r in results select r.KeyWord)
});
Note, the classes you posted would suggest that your database does not have foreign key relationships defined between the tables. If they did then this stage would be slightly simpler to write.
Stage 2
Filter out any groups that do not have each of the keywords
foreach(var keyword in splitKeywords)
{
var copyOfKeyword = keyword ; // Take copy of keyword to avoid closing over loop
grouped = (from r in grouped where r.keywords.Contains(copyOfKeyword) select r) ;
}
Stage 3
Group by Category and count the results per category
var groupedByCategories = (from r in grouped
group r by r.Category_Id into results
join c in categoryQuery on results.Key equals c.Id
select new
{
c.Id ,
c.Name ,
Count = results.Count()
});
Stage 4
Now retrieve the information from sql. This should be done all in one query.
var finalResults = groupedByCategories.ToList();
So, if I understand the need correctly, you want all of the subset of words to be matched in the text and not the OR matching you are getting right now? I see at least two options, the first of which may not translate the split to SQL:
var query = from k in keywordQuery where !splitKeywords.Except(k.Name.split(' ')).Any()
This makes the following assumptions:
Your words in the Keywords are space delimited.
You are looking for exact matches and not partial matches. (I.e. Test will not match TestTest).
The other option being to dynamically generate a predicate using predicate builder (haven't done this in a while, my implementation might need tweaking - but this is the more likely (and better in my mind) solution):
var predicate = PredicateBuilder.True<keywordQuery>();
foreach (string s in splitKeywords) {
predicate.AND(s.Contains(k.Name));
}
query.Where(predicate);
If someone can comment if some of my syntax is off I would appreciate it. EDIT: Including link to a good reference on predicate builder: http://www.albahari.com/nutshell/predicatebuilder.aspx
UPDATE
Predicate builder across multiple tables, if anyone gets here looking for how to do that.
Can PredicateBuilder generate predicates that span multiple tables?
Should be possible to query for each keyword then union the result sets. The duplicate values will be removed from the union and you can work out the required aggregations.
Try removing the class while select
var query = (from k in keywordQuery where splitKeywords.Contains(k.Name)
join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
join c in categoryQuery on kac.Category_Id equals c.Id
join a in adQuery on kac.Ad_Id equals a.Id
select new
{
Id = c.Id,
Name = c.Name,
SearchCount = keywordAdCategoryQuery.Where(s => s.Category_Id == c.Id).Where(s => s.Keyword_Id == k.Id).Distinct().Count(),
ListController = c.ListController,
ListAction = c.ListAction
}).Distinct().ToList();
var searchResults = new CategoryListByBeywordsListDto();
searchResults.CategoryListByKeywordsDetails = (from q in query select new CategoryListByKeywordsDetailDto
{
Id = q.Id,
Name = q.Name,
SearchCount = q.SearchCount,
ListController = q.ListController,
ListAction = q.ListAction
}).ToList();
You are doing a select distinct on a list of CategoryListByKeywordsDetailDto. Distinct only works on POCO and anonymous objects. In your case you need to implement the IEqualitycomparer for select distinct to work.
I tried this using LINQ directly against in memory collections (as in, not through SQL) - seems to work for me (I think the main point being that you want to search for Ads that apply to ALL the keywords specified, not ANY, correct?
Anyway, some sample code below (a little comment-ish and not necessarily the most efficient, but hopefully illustrates the point...)
Working with the following "data sets":
private List<AdCar> AdCars = new List<AdCar>();
private List<KeywordAdCategory> KeywordAdCategories = new List<KeywordAdCategory>();
private List<Category> Categories = new List<Category>();
private List<Keyword> Keywords = new List<Keyword>();
which are populated in a test method using the data you provided...
Search method looks a little like this:
var splitKeywords = keywords.Split(' ');
var validKeywords = Keywords.Join(splitKeywords, kwd => kwd.Name.ToLower(), spl => spl.ToLower(), (kwd, spl) => kwd.Id).ToList();
var groupedAdIds = KeywordAdCategories
.GroupBy(kac => kac.Ad_Id)
.Where(grp => validKeywords.Except(grp.Select(kac => kac.Keyword_Id)).Any() == false)
.Select(grp => grp.Key)
.ToList();
var foundKacs = KeywordAdCategories
.Where(kac => groupedAdIds.Contains(kac.Ad_Id))
.GroupBy(kbc => kbc.Category_Id, kac => kac.Ad_Id);
//Results count by category
var catCounts = Categories
.Join(foundKacs, cat => cat.Id, kacGrp => kacGrp.Key, (cat, kacGrp) => new { CategoryName = cat.Name, AdCount = kacGrp.Distinct().Count() })
.ToList();
//Actual results set
var ads = AdCars.Join(groupedAdIds, ad => ad.Id, grpAdId => grpAdId, (ad, grpAdId) => ad);
As I said, this is more to illustrate, please don't look too closely at the use of Joins & GroupBy etc (not sure its exactly, er, "optimal")
So, using the above, if I search for "Alfa", I get 3 Ad results, and if I search for "Alfa 147" I get just 1 result.
EDIT: I've changed the code to represent two possible outcomes (as I wasn't sure which was needed by your question)
ads will give you the actual Ads returned by the search
catCounts will give a list of anonymous types each representing the find results as a count of Ads by category
Does this help?
hi if i understand your problem correctly
"The problem is that, as I have 3 "Alfa" cars, it returns 1 + 3
records (it seems 1 for the Alfa and 147 result, and 3 for the Alfa
result)"
and Linq isn't really required i maybe have what you need just test it as new project
public Linqfilter()
{
//as Note: I modified a few classes from you because i doesn'T have your Member, Operation, Make,... classes
#region declaration
var originalAdCarList = new List<AdCar>()
{
new AdCar(){Id=1017, Title= "Alfa Romeo 145 1.6TDI 2013", Category= new Category(){Id =12}} ,
new AdCar(){Id=1018, Title= "Alfa Romeo 146 1.6TDI 2013", Category= new Category(){Id =11}} ,
new AdCar(){Id=1019, Title= "Alfa Romeo 147 1.6TDI 2013", Category= new Category(){Id =12}}
};
var originalKeywordAdCategoryList = new List<KeywordAdCategory>()
{
new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1017,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1018,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1019,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1017,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1018,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1019,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1358, Ad_Id=1017,Category_Id=1},
new KeywordAdCategory() { Keyword_Id=1373, Ad_Id=1019,Category_Id=1}
};
var originalCategoryList = new List<Category>()
{
new Category(){Id=1, Name="NULL 1 Carros"},
new Category(){Id=2, Name="NULL 1 Motos"},
new Category(){Id=3, Name="NULL 2 Oficinas"},
new Category(){Id=4 , Name="NULL 2 Stands"},
new Category(){Id=5 , Name="NULL 1 Comerciais"},
new Category(){Id=8, Name="NULL 1 Barcos"},
new Category(){Id=9 , Name="NULL 1 Máquinas"},
new Category(){Id=10 , Name="NULL 1 Caravanas e Autocaravanas"},
new Category(){Id=11 , Name="NULL 1 Peças e Acessórios"},
new Category(){Id=12 , Name="1 1 Citadino"},
new Category(){Id=13 , Name="1 1 Utilitário"},
new Category(){Id=14 , Name="1 1 Monovolume"}
};
var originalKeywordList = new List<Keyword>()
{
new Keyword(){Id=1356 ,Name="ALFA"},
new Keyword(){Id=1357 ,Name="ROMEO"},
new Keyword(){Id=1358 ,Name="145"},
new Keyword(){Id=1373 ,Name="147"}
};
#endregion declaration
string searchText = "ALFA";
// split the string searchText in an Array of substrings
var splitSearch = searchText.Split(' ');
var searchKeyList =new List<Keyword>();
// generate a list of Keyword based on splitSearch
foreach (string part in splitSearch)
if(originalKeywordList.Any(key => key.Name == part))
searchKeyList.Add(originalKeywordList.First(key => key.Name == part));
// generate a list of KeywordAdCategory based on searchKList
var searchKACList = new List<KeywordAdCategory>();
foreach(Keyword key in searchKeyList)
foreach (KeywordAdCategory kAC in originalKeywordAdCategoryList.Where(kac => kac.Keyword_Id == key.Id))
searchKACList.Add(kAC);
var groupedsearchKAClist = from kac in searchKACList group kac by kac.Keyword_Id;
var listFiltered = new List<AdCar>(originalAdCarList);
//here starts the real search part
foreach (IGrouping<int, KeywordAdCategory> kacGroup in groupedsearchKAClist)
{
var listSingleFiltered = new List<AdCar>();
// generate a list of AdCar that matched the current KeywordAdCategory filter
foreach (KeywordAdCategory kac in kacGroup)
foreach (AdCar aCar in originalAdCarList.Where(car => car.Id == kac.Ad_Id))
listSingleFiltered.Add(aCar);
var tempList = new List<AdCar>(listFiltered);
// iterrates over a temporary copie of listFiltered and removes items which don't match to the current listSingleFiltered
foreach (AdCar aC in tempList)
if (!listSingleFiltered.Any(car => car.Id == aC.Id))
listFiltered.Remove(aC);
}
var AdCarCount = listFiltered.Count; // is the count of the AdCar who match
var CatDic =new Dictionary<Category, int>(); // will contain the Counts foreach Categorie > 0
foreach(AdCar aCar in listFiltered)
if(originalCategoryList.Any(cat => cat.Id ==aCar.Category.Id))
{
var selectedCat = originalCategoryList.First(cat => cat.Id == aCar.Category.Id);
if (!CatDic.ContainsKey(selectedCat))
{
CatDic.Add(selectedCat, 1);//new Category Countvalue
}
else
{
CatDic[selectedCat]++; //Category Countvalue +1
}
}
}
}
public class Keyword
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
}
public class Category
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
}
public class KeywordAdCategory
{
//[Key]
//[Column("Keyword_Id", Order = 0)]
public int Keyword_Id { get; set; }
//[Key]
//[Column("Ad_Id", Order = 1)]
public int Ad_Id { get; set; }
//[Key]
//[Column("Category_Id", Order = 2)]
public int Category_Id { get; set; }
}
public class Ad
{
// Primary properties
public int Id { get; set; }
public string Title { get; set; }
public string TitleStandard { get; set; }
public string Version { get; set; }
public int Year { get; set; }
public decimal Price { get; set; }
// Navigation properties
public string Member { get; set; }
public Category Category { get; set; }
public IList<string> Features { get; set; }
public IList<int> Pictures { get; set; }
public IList<string> Operations { get; set; }
}
public class AdCar : Ad
{
public int Kms { get; set; }
public string Make { get; set; }
public int Model { get; set; }
public int Fuel { get; set; }
public int Color { get; set; }
}
hopefully it will help you or someone else
Edit:
extended my Methode Linqfilter() to answer the request
Edit2:
i think that should be exactly what you are looking for
var selectedKWLinq = from kw in originalKeywordList
where splitSearch.Contains(kw.Name)
select kw;
var selectedKACLinq = from kac in originalKeywordAdCategoryList
where selectedKWLinq.Any<Keyword>(item => item.Id == kac.Keyword_Id)
group kac by kac.Keyword_Id into selectedKAC
select selectedKAC;
var selectedAdCar = from adC in originalAdCarList
where (from skAC in selectedKACLinq
where skAC.Any(kac => kac.Ad_Id == adC.Id)
select skAC).Count() == selectedKACLinq.Count()
select adC;
var selectedCategorys = from cat in originalCategoryList
join item in selectedAdCar
on cat.Id equals item.Category.Id
group cat by cat.Id into g
select g;
//result part
var AdCarCount = selectedAdCar.Count();
List<IGrouping<int, Category>> list = selectedCategorys.ToList();
var firstCategoryCount = list[0].Count();
var secoundCategoryCount = list[1].Count();
Fiuu, this was brain-wreck. I splited query in several pieces, but it's executed as a whole at the end (var result). And I returned anonymous class, but intention is clear.
Here is the solution:
var keywordIds = from k in keywordQuery
where splitKeywords.Contains(k.Name)
select k.Id;
var matchingKac = from kac in keywordAdCategories
where keywordIds.Contains(kac.Keyword_Id)
select kac;
var addIDs = from kac in matchingKac
group kac by kac.Ad_Id into d
where d.Count() == splitKeywords.Length
select d.Key;
var groupedKac = from kac in keywordAdCategoryQuery
where addIDs.Contains(kac.Ad_Id)
group kac by new { kac.Category_Id, kac.Ad_Id };
var result = from grp in groupedKac
group grp by grp.Key.Category_Id into final
join c in categoryQuery on final.Key equals c.Id
select new
{
Id = final.Key,
Name = c.Name,
SearchCount = final.Count()
};
// here goes result.ToList() or similar

Categories

Resources