LINQ - convert group to array - c#

I have a problem displaying most popular tags from database in view. I'm not sure about this question title so if someone has a better one please do rename it.
Scenario
I need to show recent posts, recent galleries and most popular tags on my index page. I decided to use tuples for this and it worked fine until I tried to show most popular tags.
Error
The model item passed into the dictionary is of type
'System.Tuple3[photoBlog.Models.Gallery[],photoBlog.Models.Post[],System.Linq.IGrouping2[System.Int32,photoBlog.Models.PostTag][]]',
but this dictionary requires a model item of type
'System.Tuple`3[photoBlog.Models.Gallery[],photoBlog.Models.Post[],photoBlog.Models.PostTag[]]'.
As you can see it gives my view wrong object type. I want expect it to be an array, but it gives me some kind of System.Linq.Grouping item.
Code
As you can see I convert it into array in my controller.
public ActionResult Index()
{
photoBlogModelDataContext _db = new photoBlogModelDataContext();
var posts = _db.Posts.OrderByDescending(x => x.DateTime).Take(4).ToArray();
var galleries = _db.Galleries.OrderByDescending(x => x.ID).Take(4).ToArray();
var posttags = _db.PostTags.GroupBy(x => x.TagID).OrderBy(x => x.Count()).Take(4).ToArray();
return View(Tuple.Create(galleries, posts, posttags));
}
My view is straight forward, note that this did work until I tried to add most popular tags.
#model Tuple<photoBlog.Models.Gallery[], photoBlog.Models.Post[], photoBlog.Models.PostTag[]>
#foreach (var tag in Model.Item3)
{
#tag.Tag.Name
}

You probably want
var posttags = _db.PostTags
.GroupBy(x => x.TagID)
.OrderBy(x => x.Count())
// Take each group and pass the first tag of the group
.Select(g => g.First())
.Take(4)
.ToArray();
At the moment you're passing e.g. all 8 instances of the most popular tag, all 5 instances of the second most popular, etc., each in their own group. I imagine you just want to pass an "example" of each tag.

I don't mean to completely dodge the casting issue, but how about simplifying a little:
public class PopularStatsViewModel{
public Gallery[] Galleries { get; set; }
public Post[] Posts { get; set; }
public PostTags[] Tags { get; set; }
}
New up a PopularStats instance and set the properties to the results of your db calls. Then in your view:
#model photoblog.Models.PopularStatsViewModel
This gets you past the casting problem, sure, but more importantly it makes it a little easier to test, gives you a little more freedom in refactoring/extending that stats object, and it's pretty clear what you're working with on the view when you're accessing the VM properties.

Try changing photoBlog.Models.PostTag[] to IGrouping<TagIDType, photoBlog.Models.PostTag>[]

Related

Adding a sum to a list instead of a count

I'm using ASP.Net Core 3.1 to develop a web app. We need to return a list of values to a View. The list includes counts and sums of data. We have created a ViewModel to help. It looks like this:
public class ObjectCountViewModel
{
[DisplayName("Description")]
public string Description { get; set; }
[DisplayName("Count")]
public decimal Count { get; set; }
}
We created a list in the Controller to return the values. It looks like this:
List<ObjectCountViewModel> objectCounts = new List<ObjectCountViewModel>();
Next we added values to the list like this:
int itemsToCount = objects.Where(e => e.ObjectItems.Where(ep => ep.ObjectItemType.Description.Contains("ItemToCount") && ep.ObjectItemSelctionType.Description.Contains("Taken")).Count()>0).Count();
objectCounts.Add(new ObjectCountViewModel() { Description = "Items Counted", Count = itemsToCount });
This code works great! But we also need to generate a sum. this will be used to count items with a decimal I can't get a sum to work. Here is one of the solutions I have tried:
decimal itemToSum = objects.Where(e => e.ObjectItems.Where(ep => ep.ObjectItemType.Description.Contains("ItemToSum") && ep.ObjectItemSelectionType.Description.Contains("Taken") && ep.ObjectValueAmount>0).Sum()>0).Sum();
objectCounts.Add(new ObjectCountViewModel() { Description = "Items Taken Sum", Count = itemToSum });
I have received a wide variety of errors. The current one is: 'IEnumerable' does not contain a definition for 'Sum' and the best extension method overload 'ParallelEnumerable.Sum(ParallelQuery)' requires a receiver type of 'ParallelQuery,decimal>.
What am I doing wrong? What should my query look like for a sum?
If you have a list of lists, where you want to count all lists, then use listsOfList.SelectMany(x=>x).Count().
If you have a list of decimals, where you want a sum of all decimals, then use listsOfDecimals.Sum().
If you have a list of lists of decimals, where you want a sum of all decimals, then use listsOfListOfDecimals.SelectMany(x=>x).Sum().
I found the answer thanks to Heretic Monkey and Intellisense. I had to create a new object with the value I'm trying to sum and then filter to only select from ones that met my criteria. Then, I separated the Select statement from the Where Clause as Heretic Monkey said. Intellisense suggested I put (decimal) in front of the whole thing, and it worked! Here is my final code for this problem.
decimal itemToSum = (decimal)Objects.Where(ep => ep.RelatedObjectType.Description.Contains("Description") && ep.DifferentRelatedObjectType.Description.Contains("Description")).Select(ep => ep.itemToSum).Sum();

ASP.net MVC/EF/C# add none related table records to query in controller

I am trying to add records from table position for positionName(s) to let user select a position for employee when editing.My last attempts is to add a navigation property like field in company model
public virtual ICollection<Position> Mpositions { get; set; }
But all I got so far is null ref exception or no element in viewModel with property "PositionName" ass per viewbag didn't bother using everybody keeps recommending to avoid it so not going to do so either.
public ActionResult Edit([Bind(Include = "CompanyID,CompanyName,EntityForm,Address,Dissolute,CreationDate,FiscaleYear,Description")] Company company)
{
var GlbUpdate = db.Companies.Include(c => c.Members).Include(p => p.Mpositions);
List<Company> mdlCompanies = new List<Company>();
foreach (var item in GlbUpdate)
{
if ((item.Mpositions==null) || (item.Mpositions.Count() == 0))
{
item.Mpositions = (ICollection<Position>)new SelectList(db.Positions.Except((IQueryable<Position>)db.Positions.Select(xk => xk.Members)), "PositionID", "PositionName");
}
mdlCompanies.Add(item);
//I tried first to edit the Mpositions property directly in gblUpdate
//item.Mpositions = (IEnumerable<Position>)db.Positions.Select(p => new SelectListItem { Value = p.PositionID.ToString(), Text = p.PositionName}) ;
//(ICollection<Position>)db.Positions.ToListAsync();
}
In the view I have this
List<SelectListItem> mPositionNames = new List<SelectListItem>();
#*#this yields no results if I try gettign it from the compani record itself it gives a logic error where all id match all positionNames impossible to select an item and only positions already registered are available on the dropDownlist*#
#{foreach (var item in Model.Mpositions)
{
mPositionNames.Add(new SelectListItem() { Text = item.PositionName, Value = item.PositionID.ToString(), Selected = (false) ? true : false });
#*#selected attribute set to false not an issue, no data to select from :p so far*#
}
}
#*#null exception(if i try to midify Mpositions directly in controler) here or empty list if modify it then put it with original query in a new list*#
<div class="SectionContainer R-sectionContainerData" id="MwrapperDataRight">
#Html.DropDownListFor(mpos => item.PositionID, (SelectList)Model.Mpositions)
</div>
All I want to do is pull the positions table to create a drop downList so users can change the position of an employee but since position has a 1>many relation with employee not companies it is not bound automatically by EF nor I seem to be able to use Include() to add it.
Your query for editing positions are complex. This query must edit person's info only. Using Edit action for formalizing position's edit are not correct.It's againts to Single Responsibility Principle. Use ViewComponents for this situation. Load positions separately from person info.
I found a suitable solution using a model that encapsulate the other entities then using Partialviews/RenderAction so each part handles one entity/operation.

using string.split in entity to traverse tree depth

i have the following self-referencing table
public partial class products_category
{
public long id { get; set; }
public string category_name { get; set; }
public string category_description { get; set; }
//self referencing to table id
public Nullable<long> Parent_Id { get; set; }
public string navPath {get; set; }
}
here string navpath contains all the leading parents for a child categroy, say:
"Clothes" = 1 Parent_id=null, navpath=""
"Silk" = 2 Parent_id=1 navpath="1"
"Silk Suit"=3 parent_id=2 navpath="1-2"
"Saree" =4 parent_id=3 navpath="1-2-3"
"Dress Material"=5 parent_id=1 navpath="1" and so on....
now as per this scenario i want to access the flattend tree for frther processing for a certain depth only say to level 2 or until level 4 depth of children associated with navpath.
my idea regarding this issue was to approach using linq to ef this way:
var catTrees = db.products_category.Where(pc => pc.navpath.Split('-').Length < 4).ToList();
i am using the following link to do further traversing and tree generation:
https://bitlush.com/blog/recursive-hierarchical-joins-in-c-sharp-and-linq
and it is doing a great work so far, the only issue is i dont want to pre select whole table for processing. i want to achieve paging and a certain level of depth for first iteration, so i can maintain performance in case of thousand of records. [think of this as a category hierarchy or blog/youtube comments hierarchy].
but using the above ef linq command is giving the following error:
The LINQ expression node type 'ArrayLength' is not supported in LINQ to Entities.
i checked with ef docs and other places in SO to know that string.split doesn't work with EF implicitly. but can we apply it using extension methods or can this tree selection have alternate approach without using string.split and hitting DB only ones?
please advice.
This looks like an issues with building SQL code out of your LINQ mpre specifically SQL which takes a string splits it on dash and counts the elements.
if you dont hate the idea of loading into memory then you can force anything :)
var catTrees = db.products_category.ToList().Where(pc => pc.navpath.Split('-').Length < 4).ToList();
The trick here is to force the execution of the SQL by adding the .ToList() when we want the data from the database. This is called realizing the data.
Even with that realization trick the count is faster
var catTrees = db.products_category.ToList().Where(pc => pc.navpath.Count(a => a == '-') < 3).ToList();
these solutions are essentially the same as
List<Result> filter() {
List<Result> r = new List<Result>();
foreach(var a in db.products_category) {
if(a.navpath.Count(a => a == '-') < 3) {
r.add(a);
}
}
return r;
}
When thinking about it the filter method is somewhat less memory intensive as it reads one and one and never stores everything in memory. (in theory at least, only a few really knows what the .NET compiler does in the shadows)
I would advice you against using the navpath for checking depth.
If you can change your model, you could add an additional numeric Depth field for each category and populate it according its navpath, then you could select them from your code in this way:
var catTrees = db.products_category.Where(pc => pc.Depth < 3).ToList();
There are many ways to populate that new column, but the bottom line is that you will have to do it just once (given that you keep track of it every time you modify the navpath of a category).
One possible way of populating it would be looping through all categories, something like:
var allCategories = db.products_category.ToList();
foreach(var category in allCategories)
{
var depth = category.navpath == "" ? 0 : category.navpath.Split('-').Length + 1;
category.Depth = depth;
}
allCategories.SubmitChanges();

Remove items from list using their IDs and linq

I have a List of "widget"s (which have an int ID), and a List of selected IDs. My end goal is to have a List of hydrated widgets that agrees with the ID list inside my "set".
public List<Widget> widgets { get; set; }
public List<int> widgetIds
{
get
{
return widgets.Select(x => x.widgetId).ToList();
}
set
{
foreach (int addId in value.Except(widgets.Select(x => x.widgetId)))
{
widgets.Add(_dbWidgetHelper.getWidget(addId));
}
//I need to invert the add somehow
}
}
I've been trying everything I can think of using RemoveAll and Except, but I can't wrap my head around the solution yet. I know this can be done in one line, but the closest I've got is:
var removeIds = widgets.Select(x => x.widgetId).Except(value);
//this is me trying anything I can think of... obviously a syntax error.
widgets.RemoveAll(x=>x.widgetId in removeIds);
Try the following. Note that I changed removeIds to be a HashSet<int> to prevent the query from being re-evaluated every time and to make the look up faster
var removeIds = new HashSet<int>(widgets.Select(x => x.widgetId).Except(value));
widgets.RemoveAll(x => removeIds.Contains(x.widgetID));

Getting value from DropDownList in MVC3.. better way than binding the ID to another field?

I am wondering if there is another way to express the following bit of code. The code works as expected, but I have an issue with how it defines my naming conventions.
//Model.cs:
[DisplayName("Device Manufacturer")]
public List<KeyValuePair<int, string>> DeviceManufacturer { get; set; }
public int SelectedManufacturerID { get; set; }
//Model.ascx:
<%= Html.DropDownListFor(model => model.SelectedManufacturerID, new SelectList(Model.DeviceManufacturer, "Key", "Value"))%>
Now, whenever the selected list item is changed -- the new value is stored in SelectedManufacturerID. I am able to receive the value in my controller like so:
int selectedID = model.SelectedDataCenterID;
However, I am unhappy with the fact that MVC gives my select list the ID 'SelectedManufacturerID' when it is not an ID field -- it is a select list. Clearly this is occurring to support the binding to the SelectedManufacturerID field.
If I wish to work with my select list client-side, I now have code such as:
$('#SelectedManufacturerID').change(function(){
console.log("OnSelectedIndexChanged!");
});
This jQuery is very unclear. There is no indication that SelectedManufacturerID is a select DOM element. It seems impossible to have proper naming conventions using the MVC conventions at the top of this post. Does anyone else feel this way? Do I have other options?
If you want,you can update the ID or other html attributes of the dropdown by using a different overload of DropDownListFor helper method. Here you can pass html attributes as the last parameter.
<%= Html.DropDownListFor(model => model.SelectedManufacturerID,
new SelectList(Model.DeviceManufacturer, "Key", "Value"),
new { #id="ManufacturerList"})%>
Use select#SelectedManufacturerID instead.

Categories