var results = (from bk in bookContext.Books
orderby bk.id descending
select bk).ToList();
The table "Book" also has a genre id and if a user subscribes into a specific genre, when listing suggested books I want to order by those subscribed genres first and then order by book id. I can get the list of subscribed genre ids as a list below.
List<int> subGenereIds = new List<int>() { 10, 12, 22 };
How can add the order by section to the initial Linq query above so that users get books that have genre id in list first and the rest after that?
You'll need to to the ordering in-memory since there's not a way to pass that ordering logic to SQL. You can do that by using AsEnumerable() to change the context from a SQL query to an in-memory query:
var results = bookContext.Books
.AsEnumerable()
.OrderByDescending(bk => bk.Id)
.ThenBy(bk => subGenereIds.IndexOf(bk.Id));
From your comments it looks like you need to have as much of this as possible done in the the database. Assuming that you have the books and the user subscriptions stored in the same place you can combine the query for the user's preferences with the search of the book table to get the result you're after.
For the sake of argument, let's assume you have some tables that look something like this as POCOs:
[Table("Books")]
public class Book
{
public int ID { get; set; }
public int Genre { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public Date PublishDate { get; set; }
}
[Table("Genres")]
public class Genre
{
public int ID { get; set; }
public string Name { get; set; }
}
[Table("Users")]
public class User
{
public int ID { get; set; }
public string Name { get; set; }
}
[Table("UserGenrePreferences")]
public class UserGenrePreference
{
public int User { get; set; }
public int Genre { get; set; }
}
For the sake of the example we have one genre per book and no duplicates in the UserGenrePreferences table.
The basic ordering might look something like this:
// get genre preferences for the selected user
var userPrefs =
from pref in context.UserGenrePReferences
where pref.User == userID
select new { Genre = (int?)pref.Genre };
var result =
from book in context.Books
join pref in userPrefs on book.Genre equals pref.Genre
into prefTemp
from pref in prefTemp.DefaultIfEmpty()
orderby (pref.Genre == null ? 1 : 0), book.ID
select book;
This does an outer join against the user's genre preferences. Books with a matching genre will have a non-null value in the matching pref record, so they will be placed before unmatched books in the result. Pagination would then by handled by the normal .Skip(...).Take(...) method.
For very large data sets this will still take some time to run. If you're expecting that the user will be paging through the data it might be a good idea to grab just the book IDs and cache those in memory. Take the first few thousand results and look for more when needed for instance. Then grab the book records when the user wants them, which will be much quicker than running the query above for every page.
If on the other hand your user preference data is not in the same database, you might be able to use the Contains method to do your ordering.
Assuming that you have an array of genre IDs in memory only you should be able to do this:
int[] userPrefs = new int[] { 1, 2, 3, 5 };
var result =
from book in context.Books
orderby (userPrefs.Contains(book.Genre) ? 0 : 1), book.ID
select book;
Depending on the ORM this will translate into something similar to:
SELECT *
FROM Books
ORDER BY
CASE WHEN Genre = 1 OR Genre = 2 OR Genre = 3 OR Genre = 5 THEN 0 ELSE 1 END,
ID
This can be fairly quick but if the list of preferences is very large it could be slower.
I don't recommend that you write the more complex linq query to solve it.
Yes, it's becoming shorter that way but also becoming harder to read. The longer answer below. You can shorten it by yourself.
First you should get the ones which have genreIds of subGenereIds list.
var onesWithGenreIdFromList = (from bk in bookContext.Books
where subGenereIds.Contains(bk.genreId).orderby bk.Id descending select bk).ToList();
Then get the rest of the list (ones don't have genreId of subGenereIds list) from your list.
var results = (from bk in bookContext.Books
where !subGenereIds.Contains(bk.genreId)
orderby bk.id descending
select bk).ToList();
As the last step you should add the first list to second one to create the order you would like:
onesWithGenreIdFromList.AddRange(results);
var lastResults = onesWithGenreIdFromList;
Happy coding!
Related
I have a data model in MVC5 entity framework in which a post has a category. This category can be nested such as.
Top Level: 0
-> Lower Level: 1
-> Lowest Level: 2
This is represented in my model as:
public class CategoryModel
{
public int Id { get; set; }
public CategoryModel ParentCategory { get; set; }
public string Title { get; set; }
}
Now when I display my post which has (from the above example) category "Lowest Level 2", I would like to display
"Top level: 0 > Lower Level: 1 > Lowest Level: 2"
somewhere on that page to inform the user where they are.
Problem is I dont have any idea of how to do this.
Propably really simple (as with all things in lambda) but I don't really know how and my googling skills are really off.
Edit as per comment question:
The post is defined as this:
public class PostModel
{
public int Id { get; set; }
public CategoryModel Category { get; set; } // a post can only have one category
public string Text { get; set; }
}
What I want to do is follow the CategoryModel relation, and then keep following the Categories ParentCategory untill it is null. This is always a 1 to 1 relation.
More Edit:
I was fairly simply able to do this with a TSQL-CTE expression but still no idea how to convert this to lambda.
SQL:
;WITH Hierarchy(Title, CatId, LEVEL, CategoryPath)
AS
(
Select c.Title, c.Id, 0, c.Title
FROM Categories c
WHERE c.[ParentCategory_Id] IS NULL
UNION ALL
SELECT c.Title, c.Id, H.LEVEL+1, H.CategoryPath+' > '+c.Title
FROM Categories c
INNER JOIN Hierarchy H ON H.CatId = c.[ParentCategory_Id]
)
SELECT SPACE(LEVEL*4) + H.Title, *
FROM Hierarchy H
ORDER BY H.CategoryPath
Result:
Assuming you have an instance of CategoryModel you could write a function that will build a string list with the chain of all titles:
private void FormatCategories(CategoryModel model, List<string> result)
{
result.Add(model.Title);
if (model.ParentCategory != null)
{
FormatCategories(model.ParentCategory, result);
}
}
and then:
CategoryModel model = ... // fetch your model from wherever you are fetching it
var result = new List<string>();
FormatCategories(model, result);
Now all that's left is to reverse the order of elements in the list and join them to retrieve the final result:
result.Reverse();
string formattedCategories = string.Join(" -> ", result);
// At this stage formattedCategories should contain the desired result
What i am trying to do is i have sales order object which contains
sales order header
and list of order lines
within the order lines i have the actual order line, product information object and stock information object:
public class SalesOrder
{
public Header SalesHeader { get; set; }
public List<OrderLineProductInfo> OrderLines { get; set; }
}
public class OrderLineProductInfo
{
public Line salesOrderLine { get; set; }
public Info ProductInfo { get; set; }
public Stock ProductStock { get; set; }
}
so i can get a list of SalesOrder Objects so example sales order index 0
has 2 lines the ProductStockObject within one of these lines has Preferred Supplier of abc and the other line has Preferred Supplier 123
i want to be able to group on the Preferred Supplier property
var separatePreferredSuppliers =
(from b in x.OrderLines
.GroupBy(g => g.ProductStock.PreferredSupplier )
select ...
).ToList();
not quite sure what comes next what needs to be selected? a new list of SalesOrder?
I want it so that it gives two instances of the sales order but split in 2 one for each preferred supplier
I think I get what you mean
from line in x.OrderLines
group line by line.ProductStock.PreferredSupplier into grouped
select new SalesOrder
{
OrderLines = grouped.ToList()
}
though I'm not sure how you populate your SalesHeader
easy when you know what the groupby function ruturns -- you want the key and the list:
.Select( (g) => new { supplier = g.Key, prodlist = g.ToList()}
I am using EntityFramework 6 and running into some major speed issues -- this query is taking over two seconds to run. I have spent the better part of the day using LinqPad in order to speed up the query but I could only get it down from 4 to two seconds. I have tried grouping, joins, etc. but the generated SQL looks overly complicated to me. I am guessing that I am just taking the wrong approach to writing the LINQ.
Here is what I am attempting to do
Find all A where Valid is null and AccountId isn't the current user
Make sure the Collection of B does not contain any B where AccountId is the current user
Order the resulting A by the number of B in its collection in descending order
Any A that doesn't have any B should be at the end of the returned results.
I have to models which look like this:
public class A
{
public int Id { get; set; }
public bool? Valid { get; set; }
public string AccountId { get; set; }
public virtual ICollection<B> Collection { get; set; }
}
public class B
{
public int Id { get; set; }
public bool Valid { get; set; }
public string AccountId { get; set; }
public DateTime CreatedDate { get; set; }
public virtual A Property { get; set; }
}
The table for A has about one million rows and B will eventually have around ten million. Right now B is sitting at 50,000.
Here is what the query currently looks like. It gives me the expected results but I have to run an orderby multiple times and do other unnecessary steps:
var filterA = this.context.A.Where(gt => gt.Valid == null && !gt.AccountId.Contains(account.Id));
var joinedQuery = from b in this.context.B.Where(gv => !gv.AccountId.Contains(account.Id))
join a in filterA on gv.A equals a
where !a.Collection.Any(v => v.AccountId.Contains(account.Id))
let count = gt.Collection.Count()
orderby count descending
select new { A = gt, Count = count };
IQueryable<GifTag> output = joinedQuery
.Where(t => t.A != null)
.Select(t => t.A)
.Distinct()
.Take(20)
.OrderBy(t => t.Collection.Count);
Thanks
Well you could always try to remove these two lines from the joinQuery
where !a.Collection.Any(v => v.AccountId.Contains(account.Id))
and
orderby count descending
the first line have already been filtered in the first Query
and the orderline, well do do the ordering on the last Query so there is no point in doing it twice
I want select the all restaurants, and for the each restaurant load the list of the attached categories.
There is a many-to-many relationship between the Restaurant and Category:
public class Restaurant
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Value { get; set; }
}
In my current implementation I'm selecting a raw data with all restaurants and categories, and process them on the client side: group by restaurant and select categories. In this case, the generated SQL looks very simple and executed fast:
var plainData = (
from restaurant in RestaurantsRepository.GetAll()
from category in restaurant.Categories
select new
{
Id = restaurant.Id,
Name = restaurant.Name,
Category = category.Value
}).ToList();
var restaurants = (
from restaurant in plainData
group restaurant by new
{
restaurant.Id,
restaurant.Name
}
into grp
select new RestaurantModel
{
Id = grp.Key.Id,
Name = grp.Key.Name,
Categories = grp.Select(c => c.Category).ToList()
});
The another variant is to use Entity Framework and the relation between restaurants and categories. In this case, the generated SQL is very complicated and executed four times slower:
var restaurants =(
from restaurant in RestaurantsRepository.GetAll()
select new RestaurantModel
{
Id = restaurant.Id,
Name = restaurant.Name,
Categories = restaurant.Categories
}).ToList();
The question is: There is there a more efficient way (then 1 or 2) to select my data?
Your collection is virtual, thus, I suppose, you're using lazy loading.
The second variant executes N + 1 queries, where N is a count of items, returned from RestaurantsRepository.GetAll(): one query to get all restaurants, and N queries to get all categories for the particular restaurant.
Try to use eager loading of collection:
RestaurantsRepository
.GetAll()
.Include(r => r.Categories)
This should execute single query with JOIN against database, like this (real SQL will differ):
SELECT
*
FROM
[Restaurants] JOIN [Categories] ON [Restaurants].Id = [Categories].[RestaurantId]
Also, think about lazy loading - do you really need it, if you're mapping query result into another types (RestaurantModel in your sample).
In .NET4 MVC 4 using Code First I have two classes that form a many to many relationship.
How can I write a simple LINQ query to find all Games that have at least one or more genres from a given list of genres? (i.e. Retreive all games that are of the Strategy and/or Sandbox genre)
public class Game
{
public int ID { get; set; }
public virtual ICollection<Genre> Genres { get; set; }
}
public class Genre
{
public int ID { get; set; }
public virtual ICollection<Game> Games { get; set; }
}
Please note that I am using Code First so I have no access to the abstracted join table GenreGames.
I have tried the following to find a game that contains at least one genre ID from the array gid:
var gid = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = from g in unitOfWork.GameRepository.dbSet
where (
from gen in unitOfWork.GenreRepository.dbSet
where gid.Contains(gen.ID)
select gen.ID
).Any()
select g;
When retrieving the results
List<Game> similiarGames = result.ToList<Game>();
the following error comes up.
Unable to create a constant value of type 'GratifyGaming.Domain.Models.Genre'. Only primitive types or enumeration types are supported in this context.
How can I get the results I want using only primitive or enumeration types? Is there a workaround?
Try this instead :
var test = unitOfWork.GameRepository.dbSet
.Where(G => G.Genres.Where(genre =>
gid.Contains(genre .ID)).Count() > 0);
Good Luck !!
Try this:
var result = unitOfWork.GameRepository.dbSet
.Where(game => game.Genres.Any(genre => gid.Contains(genre.ID));