I have this mvc controller:
public ActionResult Index(string country)
{
var c = _hc.Countries.Where(l => l.Code.Equals(country, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
var customers = _hc.Customers.Where(h => h.Country.Code.Equals(country, StringComparison.InvariantCultureIgnoreCase));
foreach (var h in customers)
h.Country = c;
return View(customers);
}
Everything works, but I just want the Customer.Country.Code to have a value. And since this one lists for all customers with the same country...
Same for when I use a raw SqlQuery (because of the geolocation stuff)
var customers = _hc.Database.SqlQuery<Customer>(#"
SELECT customers.*, countries.code
FROM [dbo].customers
inner join countries on customers.countryid = countries.id
where geography::Point(50.436912, 5.972050, 4326).STDistance(geography::Point([Latitude], [Longitude], 4326)) <= 25000").ToList();
I have the countryid in the customer object, because that is the Foreign Key. And have seen some extension method which you could use to include other data. But according to my intellisens, it's not availble. Probably something easy. But too hard for a friday afternoon (for me).
In order to use the Include() extension method you need to add the following:
using System.Data.Entity;
See DbExtensions.Include
Example usage:
var customers = _hc.Customers
.Include(h => h.Country)
.Where(h => h.Country.Code.Equals(country, StringComparison.InvariantCultureIgnoreCase));
Related
I have object CartItem and Product.
I need to make one List<> from these 2 objects.
Tried:
List<(CartItem,Product)> product = new List<(CartItem,Product)>();
Code:
public async Task<IActionResult> Index()
{
var username = User.Identity.Name;
if (username == null)
{
return Login();
}
var user = _context.Users.FirstOrDefault(x => x.Email == username);
List<CartItem> cart = _context.ShoppingCartItems.Where(s => s.IDuser.Equals(user.Id)).ToList();
List<Product> product = new List<Product>();
foreach (var item in cart)
{
product.Add(_context.Product.FirstOrDefault(x => x.id == item.ProductId));
}
return View(user);
}
The correct answer would be to not do that. What would be the expected result for 5 items and 2 products?
The code is loading both CartItem and Product objects from the database using loops. That's very slow as each object requires yet another query. This can be replaced with a single line producing a single SQL query.
If CartItem has Product and User properties (as it should) all the code can be replaced with :
var cart=_context.ShoppingCartItems
.Include(i=>i.Product)
.Where(s=>s.User.Email==userName)
.ToList();
EF Core will generate the SQL query that joins User, CartItem, Product together and returns items and their products, but no User data. The Product data will be available through the CartItem.Product property
What was asked but shouldn't be used
If a List<(CartItem,Product)> is absolutely required (why???) you could use that instead of a List<Product>, and add items inside the loop:
// PLEASE don't do it this way
var dontUseThis = new List<(CartItem,Product?)>();
foreach (var item in cart)
{
var product=_context.Product.FirstOrDefault(x => x.id == item.ProductId);
dontUseThis.Add((item,product));
}
This will result in one extra SQL query per cart item.
Slightly better
A slightly better option would be to generate a WHERE product.ID IN (....) clause, to load all relevant products in a single query. After that the two lists would have to be joined with JOIN.
var productIds=cart.Select(c=>c.ProductId);
var products=_context.Product
.Where(p=>productIds.Contains(p.Id))
.ToList();
var dontUseThis = products.Join(cart,
p => p.Id,
c => c.ProductId,
(c, p) => (c,p))
.ToList();
This will reduce the N+1 queries to 2. It's still slower than letting the database do the work though
First, see the answer of #Panagiotis Kanavos. Aside that, for combining part, you can do this:
List<CartItem> cartItems; // assuming you already have this
List<Product> products; // assuming you already have this
// The combination part
var result = from p in products
join ci in cartItems on p.Id = ci.ProductId // Find out how product and cartItem relates
select new (p,ci);
// Need List?
var resultingList = result.ToList();
You can make a struct and put both variables in there:
struct EntireProduct
{
CartItem cartItem;
Product product;
}
then you can create a list of that struct:
List<EntireProduct> product = new List<EntireProduct>();
Sorry for strange title of the question, but I don't know how to formulate it more short. If you know how to formulate it better, I will be glad if you edit my question.
So, I have the following table:
I'm tolking about CustomerId and EventType fields. The rest is not important. I think you understand that this table is something like log by customers events. Some customer make event - I have event in the table. Simple.
I need to choice all customers events where each customer had event with type registration and type deposit. In other words, customer had registration before? The same customer had deposit? If yes and yes - I need to select all events of this customer.
How I can do that with the help of LINQ?
So I can write SQL like
select *
From "CustomerEvents"
where "CustomerId" in (
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
)
It works, but how to write it on LINQ?
And second question. SQL above works, but not it is not universal. What if tomorrow I will need to show events of customers who have registration, deposit and - new one event - visit? I have to write new one query. Like:
select *
From "CustomerEvents"
where "CustomerId" in (
select "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'visit'
)
Uncomfortable :(
As source data, I have List with event types. Is there some way to make it dynamically? I mean, I have new one event in the list - I have new one intersect.
P.S I use Postgres and .NET Core 3.1
Update
I pine here a scheme
I haven't tested to see if this will translate to SQL correctly, but if we assume ctx.CustomerEvents is DbSet<CustomerEvent> you could try this:
var targetCustomerIds = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId)
.Where(grouped =>
grouped.Any(event => event.EventType == "deposit")
&& grouped.Any(event => event.EventType == "registration"))
.Select(x => x.Key)
.ToList();
and then select all events for these customers:
var events = ctx.CustomerEvents.Where(event => targetCustomerIds.Contains(event.CustomerId));
To get targetCustomerIds dynamically with a variable number of event types, you could try this:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID
var groupedByCustomerId = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId);
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You can define the GetFilteredGroups method like this:
private static IQueryable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IQueryable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
Alternatively, instead of selecting the target customer IDs, you can try to directly select your target events from the filtered groupings:
// ...
// Filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Select your events here
var results = filtered.SelectMany(x => x).Distinct().ToList();
Regarding the inability to translate the query to SQL
Depending on your database size and particularly on the size of CustomerEvents table, this solution may or may not be ideal, but what you could do is load the optimized collection to memory and perform the grouping there:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID, but load into memory
var groupedByCustomerId = ctx
.CustomerEvents
.Where(event => requiredEventTypes.Contains(event.EventType))
.Select(event => new CustomerEventViewModel
{
Id = event.Id,
CustomerId = event.CustomerId,
EventType = event.EventType
})
.GroupBy(event => event.CustomerId)
.AsEnumerable();
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You will need to create a type called CustomerEventViewModel like this (so you don't have to load the entire CustomerEvent entity instances to memory):
public class CustomerEventViewModel
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string EventType { get; set; }
}
And change the GetFilteredGroups like this:
private static IEnumerable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IEnumerable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
It should now work fine.
Thank you for #Dejan Janjušević. He is excpirienced developer. But it seems EF can't translate him solution to SQL (or just my hands grow from wrong place). I publish here my solution for this situation. It's simple stuped. So. I have in the table EventType. It is string. And I have from the client the following filter request:
List<string> eventType
Just list with event types. So, in the action I have the following code of the filter:
if (eventType.Any())
{
List<int> ids = new List<int>();
foreach (var e in eventType)
{
var customerIdsList =
_context.customerEvents.Where(x => x.EventType == e).Select(x => x.CustomerId.Value).Distinct().ToList();
if (!ids.Any())
{
ids = customerIdsList;
}
else
{
ids = ids.Intersect(customerIdsList).ToList();
}
}
customerEvents = customerEvents.Where(x => ids.Contains(x.CustomerId.Value));
}
Not very fast, but works.
I am trying to link up the RestaurantId in the RestaurantReservationEventsTbl with the RestaurantID in the RestaurantTbl to display reservations that are only made for the currently logged in restaurant.
I am receiving the following error in my code operator == cannot be applied to operands of type int and iqueryable int
Here is what I am doing in my home controller
var RestaurantIDRestaurantTbl = from r in db.Restaurants select r.RestaurantID;
//var listOfRestaurantsReservations = db.RestaurantReservationEvents.ToList();
var listOfRestaurantsReservations = db.RestaurantReservationEvents.Where(x => x.RestaurantID == RestaurantIDRestaurantTbl).ToList();
//return View(restaurants.Where(x => x.RestaurantEmailAddress == UserEmail).ToList());
//create partial view called _RestaurantReservation
return PartialView("_RestaurantReservations", listOfRestaurantsReservations);
You have to change your code to materialize the restaurantIds like this:
var RestaurantIDRestaurantTbl = (from r in db.Restaurants
select r.RestaurantID).ToList();
Then you may change the code as below for the comparison to work:
var listOfRestaurantsReservations = db.RestaurantReservationEvents.Where(x => RestaurantIDRestaurantTbl.Contains(x.RestaurantID)).ToList();
Anyway this is not the best solution. I will write another example for you, just try this example if it is working or not and let me know for the result.
I would considering changing the code as below to be much more efficient:
var listOfRestaurantsReservations = (from r in db.Restaurants
join e in db.RestaurantReservationEvents
on r.RestaurantID equals e.RestaurantID
//where r.RestaurantID == something //if where condition needed
select e).ToList();
If your tables are not connected with foreignkeys please consider to read this documentation here to make a better structure of the tables since they are related to each-other.
If your tables are related as in documentation article you might have something like that:
var RestaurantIDRestaurantTbl = db.Restaurants.SingleOrDefault(x => x.RestaurantID == something);
if(RestaurantIDRestaurantTbl != null)
{
var listOfRestaurantsReservations = RestaurantIDRestaurantTbl.RestaurantReservationEvents.ToList();
}
{
// This will give you a list of IDs
var RestaurantIDRestaurantTbl = db.Restaurants
.Select(p => p.RestaurantID)
.ToList();
// Using .Any() is a better choice instead of .Contains()
// .Contains is used to check if a list contains an item while .Any will look for an item in a list with a specific ID
var listOfRestaurantsReservations = db.RestaurantReservationEvents
.Where(p => RestaurantIDRestaurantTbl.Any(r => r.pRestaurantID == p))
.ToList();
}
Having the following example:
var myIds = db.Table1.Where(x=>x.Prop2 == myFilter).Select(x=>x.Id).ToList();
var results = db.Table2.Where(x=> myIds.Contains(x.T1)).ToList();
This part is straight forward.
However, now I am facing a "slight" change where my "filter list" has 2 properties instead of only one:
// NOTE: for stackoverflow simplification I use a basic query to
// get my "myCombinationObject".
// In reality this is a much more complex case,
// but the end result is a LIST of objects with two properties.
var myCombinationObject = db.Table3.Where(x=>x.Prop3 == myFilter)
.Select(x=> new {
Id1 = x.T1,
Id2 = x.T2
}).ToList();
var myCombinationObjectId1s = myCombinationObject.Select(x=>xId1).ToList();
var myCombinationObjectId2s = myCombinationObject.Select(x=>xId2).ToList();
// step#1 - DB SQL part
var resultsRaw = db.Tables.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.ToList();
// step#2 - Now in memory side - where I make the final combination filter.
var resultsFiltered = resultsRaw.Where( x=>
myCombinationObject.Contains(
new {Id1 = x.Prop1, Id2 = x.Prop2 }
).ToList();
My question: is it even possible to merge the step#2 in the step#1 (query in linq to entities) ?
I've managed once to do what you want, however it is pretty hard and requires changing entity model a bit. You need an entity to map type
new {Id1 = x.Prop1, Id2 = x.Prop2 }
So you need enity having 2 properties - Id1 and Id2. If you have one - great, if not then add such entity to your model:
public class CombinationObjectTable
{
public virtual Guid Id1 { get; set; }
public virtual Guid Id2 { get; set; }
}
Add it to your model:
public DbSet<CombinationObjectTable> CombinationObjectTable { get; set; }
Create new migration and apply it database (database will have now additional table CombinationObjectTable). After that you start to build a query:
DbSet<CombinationObjectTable> combinationObjectTable = context.Set<CombinationObjectTable>();
StringBuilder wholeQuery = new StringBuilder("DELETE * FROM CombinationObjectTable");
foreach(var obj in myCombinationObject)
{
wholeQuery.Append(string.Format("INSERT INTO CombinationObjectTable(Id1, Id2) VALUES('{0}', '{1}')", obj.Id1, obj.Id2);
}
wholeQuery.Append(
db.Tables
.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.Where( x=>
combinationObjectTable.Any(ct => ct.Id1 == x.Id1 && ct.Id2 == x.Id2)
).ToString();
);
var filteredResults = context.Tables.ExecuteQuery(wholeQuery.ToString());
Thanks to this your main query stays written in linq. If you do not want to add new table to your db this is as well achievable. Add new class CombinationObjectTable to model, generate new migration to add it and afterwards remove code creating that table from migration code. After that apply migration. This way the db schema won't be changed but EF will think that there is CombinationObjectTable in database. Instead of it you will need to create a temporary table to hold data:
StringBuilder wholeQuery = new StringBuilder("CREATE TABLE #TempCombinationObjectTable(Id1 uniqueidentifies, Id2 uniqueidentifier);");
And when you invoke ToString method on your linq query change CombinationObjectTable to #TempCombinationObjectTable:
...
.ToString()
.Replace("CombinationObjectTable", "#TempCombinationObjectTable")
Other thing worth considering would be using query parameters to pass values in INSERT statements instead of just including them in query yourself - this is of course achievable with EF as well. This solution is not fully ready to apply, rather some hint in which direction you may go for the solution.
Can you do something like this:
var result=
db.Tables
.Where(t=>
db.Table3
.Where(x=>x.Prop3 == myFilter)
.Any(a=>a.T1==t.Prop1 || a.T2==t.Prop2)
).ToList();
If you simply want to avoid the intermediate result (and also creating a second intermediary list) you can do the following
var resultsFiltered = db.Tables.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.AsEnumerable() // everything past that is done in memory but isn't materialized immediately, keeping the streamed logic of linq
.Where( x=>
myCombinationObject
.Contains(new {Id1 = x.Prop1, Id2 = x.Prop2 })
.ToList();
I'm trying to associate two entity types in EF4, there is no foreign key available.
Is there any manual tweaks I can do to so when I load an instance of order I get something like:
order.orderID = 455
order.date = 2012-08-12
order.OrderItems = <List>OrderItem // OrderItem being the other entity I want to map on order
I guess I'll have to do this manually when I Select() an order and set it's OrderItems property since no FKs are avail.
Am I on the right track ?
**EDIT:
I was able to create an association between orders and orderitems and here's a Select() method I have for an order:
public Order Select(long orderID)
{
using (var ctx = new BillingSystemEntities())
{
var res = from n in ctx.Orders
where n.OrderID == orderID
select n;
if (res.Count() == 0)
return null;
else
return res.First();
}
}
According to SQL Profiler, EF is not doing any JOIN on OrderItem table. I guess I need to load them into the OrderItems navigation property myself ?
You just need to include the OrderItems in your query:
public Order Select(long orderID)
{
using (var ctx = new BillingSystemEntities())
{
var res = from n in ctx.Orders.Include("OrderItems")
where n.OrderID == orderID
select n;
return res.FirstOrDefault();
}
}
Also FirstOrDefault is more appropriate here than your Count()...First() construct because it hits the database only once while Count() and First() will issue a separate query each.
You can write it even more compact with:
public Order Select(long orderID)
{
using (var ctx = new BillingSystemEntities())
{
return ctx.Orders.Include("OrderItems")
.FirstOrDefault(n => n.OrderID == orderID);
}
}