Convert nested relational model to a string - c#

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

Related

Filtering parent collection by grandchildren properties in EF Core

Using EF core 5 and ASP.NET Core 3.1, I am trying to get a filtered collection based on a condition on its grandchildren collection.
I have the following entities:
public class Organisation
{
public int Id { get; set; }
public int? OrganisationId { get; set; }
public IEnumerable<Customer> Customers { get; set; }
}
public partial class Customer
{
[Key]
public uint Id { get; set; }
public int? EmployerId { get; set; }
public int? OrganisationId { get; set; }
public List<TimecardProperties> TimecardsProperties { get; set; }
}
public partial class TimecardProperties
{
[Key]
public int Id { get; set; }
public int? EmployerId { get; set; }
public int? Week { get; set; }
public short? Year { get; set; }
}
The goal is to get all Organisations that have at least one customer and the customer has at least 1 timecard property that is in week=34 and year=2021.
So far I have tried the following:
////necessary join to get Organisations for user id
IQueryable<Organisation> ouQuery = (from cou in _dbContext.Organisations
join uou in _dbContext.table2 on cou.OrganisationId equals uou.OrganisationId
where uou.UsersId == int.Parse(userId)
select cou)
.Where(cou => cou.Customers.Where(c => c.TimecardsProperties.Count > 0).Any())
.Include(cou => cou.Customers.Where(c => c.TimecardsProperties.Count > 0))
.ThenInclude(c => c.TimecardsProperties.Where(tc => tc.tWeek == 34 && tc.Year > 2020))
;
This returns a organisation list that each have a customers list but some customers have a count of timecards 0. I don't want to have organisation in the returned list that does not have at least one item in the timecards collection.
Also, it is too slow, and if I try to filter the produced list its even
slower (over 15 seconds)
I have also tried a raw sql query on the organisation db context but it is again very slow:
select distinct count(id) from organisation a where organisation_id in (
select organisation_id from customers where employer_id in (select distinct employer_id from timecards a
inner join timecard_components b on a.id=b.timecards_id
where week IN(
34) and year in (2021,2021) and invoice !=0 and type = 'time'
group by employer_id, week)
);
In general, I want to know the the total
count of the returned organisation collection for pagination (so I don't need to include all attributes of each entity)
as well as return only a part of the correct results, which satisfy the conditions,
an organisation list that has at least 1 timecards in
their customers by executing the query in the end like so:
ouQuery.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
I have also tried the EntityFramework.Plus and projection with no results.
How could I write this to achieve getting the total count of the organisation list and a part of these results (first 10) to display to the user?
Use navigation properties. This is the query you want:
var orgsQuery = dbContext.Organizations
.Where( o => o.Customers.Any( c =>
c.TimecardProperties.Any( tp =>
tp.Year = 2021
&& tp.Week = 34 ) ) );
Add includes and other predicates as needed

Using external list to order a linq query

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!

RavenDB many to many and indexes

Need a help with RavenDB.
In my web page I want to have such list:
item1 category1
item2 category2
...
and another one:
category1, number of items
category2, number of items
...
My data structures:
public class Item
{
public string Id { get; set; }
public string Name { get; set; }
public string CategoryId { get; set; }
}
public class Category
{
public string Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
Index for the first list:
public class Item_WithCategory : AbstractIndexCreationTask<Item>
{
public class Result
{
public string Name { get; set; }
public string CategoryName { get; set; }
}
public Item_WithCategory()
{
Map = items => from item in items
select new
{
Name = item.Name,
CategoryName = LoadDocument<Category>(item.CategoryId).Name
};
}
}
Is this data structure suitable for my case, or will it be better to have Category instead of CategoryId in item structure?
Should I use my index or is there a better solution to take category name?
If my index is good, how to write a correct query? My current try:
Item_WithCategory.Result[] all;
using (var session = DocumentStoreHolder.Store.OpenSession())
{
all = session.Query<Item_WithCategory.Result, Item_WithCategory>().ToArray();
}
but it throws exception stating that return type is item, not result. How to fix it?
You have a couple of options here. You could store both the CategoryId and the CategoryName on the Item entity. This will of course lead to duplicated data (if you still need to store the Category entity), but "storage is cheap" is a popular term these days.The downside of this is that you need to update each Item document of a given category if the category name changes to keep things consistent. A benefit is that you need to do less work to get your desired result.
If you store Category Name on the item as well you don't need a special index to handle the first list, just query on the Items and return what you need. For the second list you need to create a Map/Reduce index on the Item entity that groups on the category.
However, if you need to use the data structure you've given, there are a couple of ways of solving this. First, it's really not recommended to use a LoadDocument inside of an index definition, especially not in a select statement. This might affect indexing performance in a negative way.
Instead, just index the properties you need to query on (or use an auto index) and then use a Result Transformer to fetch information from related documents:
public class ItemCategoryTransformer : AbstractTransformerCreationTask<Item>
{
public ItemCategoryTransformer()
{
TransformResults = results => from item in results
let category = LoadDocument<Category>(item.CategoryId)
select new ItemCategoryViewModel
{
Name = item.Name,
CategoryName = category.Name
};
}
}
public class ItemCategoryViewModel
{
public string Name { get; set; }
public string CategoryName { get; set; }
}
You can use this Transformer with a Query on the Item entity:
using (var session = documentStore.OpenSession())
{
var items = session.Query<Item>()
.TransformWith<ItemCategoryTransformer, ItemCategoryViewModel>()
.ToList();
}
As for the second list, still using your data structure, you have to use a couple of things. First, a Map/Reduce index over the Items, grouped by CategoryId:
public class Category_Items_Count : AbstractIndexCreationTask<Item, Category_Items_Count.Result>
{
public class Result
{
public string CategoryId { get; set; }
public int Count { get; set; }
}
public Category_Items_Count()
{
Map = items => from item in items
select new Result
{
CategoryId = item.CategoryId,
Count = 1
};
Reduce = results => from result in results
group result by result.CategoryId
into c
select new Result
{
CategoryId = c.Key,
Count = c.Sum(x => x.Count)
};
}
}
But as you only have the CategoryId on the Item entity, you have to use a similar transformer as in the first list:
public class CategoryItemsCountTransformer : AbstractTransformerCreationTask<Category_Items_Count.Result>
{
public CategoryItemsCountTransformer()
{
TransformResults = results => from result in results
let category = LoadDocument<Category>(result.CategoryId)
select new CategoryItemsCountViewModel
{
CategoryName = category.Name,
NumberOfItems = result.Count
};
}
}
public class CategoryItemsCountViewModel
{
public string CategoryName { get; set; }
public int NumberOfItems { get; set; }
}
And lastly, query for it like this:
using (var session = documentStore.OpenSession())
{
var items = session.Query<Category_Items_Count.Result, Category_Items_Count>()
.TransformWith<CategoryItemsCountTransformer, CategoryItemsCountViewModel>()
.ToList();
}
As you can see, there are quite a difference in work needed depending on what data structure you're using. If you stored the Category Name on the Item entity directly you wouldn't need any Result Transformers to achieve the results you're after (you would only need a Map/Reduce index).
However, Result Transformers are executed server side and are only executed on request, instead of using LoadDocument inside of an index which is executed every time indexing occurs. Also, and maybe why LoadDocuments inside of index definitions isn't recommended, every change to a document that's referenced with a LoadDocument in an index will cause that index to have to be rewritten. This might lead to a lot of work for the index engine.
Lastly, to answer your last question about why you get an exception when querying: As the actual return type of your index is the document that that's being indexed (in this case Item). To use something else you need to project your result to something else. This can be done by using ".As()" in the query:
Item_WithCategory.Result[] all;
using (var session = DocumentStoreHolder.Store.OpenSession())
{
all = session.Query<Item_WithCategory.Result, Item_WithCategory>()
.As<Item_WithCategory.Result>()
.ToArray();
}
Long post, but hope it helps!

Group By object within a nested list linq

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()}

Entity Framework 4.1 count identical database rows

My setup
I have two classes, here shown stripped down to what is needed in this example:
public class Photo : Entity
{
[Key]
public int Id { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public Photo()
{
Tags = new List<Tag>();
}
}
public class Tag : Entity
{
[Key]
public string Text { get; set; }
public virtual ICollection<Photo> Photos { get; set; }
public Tag()
{
Photos = new List<Photo>();
}
}
As shown above theres is a many-to-many relationship between the two entities.
I'm using EF 4.1 code-first.
Example:
"photo1" has "tag1", "tag2" and "tag3" in its tags navigation property.
"photo2" has "tag2", "tag3" and "tag4" in its tags navigation property.
"photo3" has "tag2" and "tag4" in its tags navigation property.
Total tag count in all photos:
"tag1": 1
"tag2": 3
"tag3": 2
"tag4": 2
Total tags: 8
Note
My end goal is this tag cloud, but using MVC3:
http://www.geekzilla.co.uk/View960C74AE-D01B-428E-BCF3-E57B85D5A308.htm
First question
How do I (using EF) find out how many times the most used tag(s) is used (finding the count of "tag2")?
And the same for the least used tag(s) (count of "tag1" in above example).
In the link above these to lines of code is used:
double.TryParse(tagData.Compute("min(count)", null).ToString(), out min);
double.TryParse(tagData.Compute("max(count)", null).ToString(), out max);
What is the EF/LINQ equivalent?
Second question
How do I get the count for each tag or the count for the 50 most used tags?
Counts by tag:
from t in Context.Tags
select new
{
t.Text,
t.Photos.Count()
}
Most used tag:
(from t in Context.Tags
let photoCount = t.Photos.Count()
orderby photoCount descending
select new
{
t.Text,
photoCount
}).FirstOrDefault()
50 most used tags (may not actually have 50 there):
(from t in Context.Tags
let photoCount = t.Photos.Count()
orderby photoCount descending
select new
{
t.Text,
photoCount
}).Take(50)
Freehand, so might not be 100% syntactically correct or the smallest/most readable way to do it.
Edit: Added Example:
foreach(var result in Context.Tags.Select(x => new { t.Text, t.Photos.Count() }))
{
Console.WriteLine(string.Format("Text: {0}, Count: {1}", result.Text, result.Count.ToString());
}

Categories