How to use GroupBy using Dynamic LINQ - c#

I am trying to do a GroupBy using Dynamic LINQ but have trouble getting it to work.
This is some sample code illustrating the problem:
List<dtoMyAlbum> listAlbums = new List<dtoMyAlbum>();
for (int i = 0; i < 5000; i++)
{
dtoMyAlbum album = new dtoMyAlbum
{
Author = "My Author",
BookID = i,
CurrSymbol = "USD",
Price = 23.23,
Shop = i % 3 == 0 ? "TESCO" : "HMV"
};
listAlbums.Add(album);
}
IQueryable<dtoMyAlbum> mydata = listAlbums.AsQueryable();
int count = mydata.Count();
//var mydataGrouped = mydata.GroupBy(a => a.Shop); // <-- this works well (but is not dynamic....)
var mydataGrouped = mydata.GroupBy("Shop"); // <-- does not compile but is kind of what I want...
foreach (var group in mydataGrouped)
{
//count = group.Count();
}
I realise that I am missing the 'elementSelector' in the GroupBy overload but all I want to do is to end up with (in this case) two sets of dtoMyAlbum objects so I wish to select ALL elements for all sets...
How would I go about this?

There is default it defined, you can use it to return matched elements:
var mydataGrouped = mydata.GroupBy("Shop", "it");
To iterate through results you should additionally Select elements to name it and use dynamics:
var mydataGrouped = mydata.GroupBy("Shop", "it").Select("new (it.Key as Shop, it as Albums)");
foreach (dynamic group in mydataGrouped)
{
foreach (dynamic album in group.Albums)
{
Console.WriteLine(album.Author);
}
}

You may construct the group by expression dynamically or give a try to this Dynamic LINQ library presented on ScottGu's page:
http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

This is the method I have copied from DynamicQuery:
public static IQueryable GroupBy(this IQueryable source, string keySelector, string elementSelector, params object[] values) {..}
In my example I provide the keySelector and the elementSelector.
listAlbums.AsQueryable().GroupBy("it.Shop", "it").Select("Author"); You can use new with GroupBy or with select for the new types.
It like this in OOP class.

Related

Merge data from two arrays or something else

How to combine Id from the list I get from file /test.json and id from list ourOrders[i].id?
Or if there is another way?
private RegionModel FilterByOurOrders(RegionModel region, List<OurOrderModel> ourOrders, MarketSettings market, bool byOurOrders)
{
var result = new RegionModel
{
updatedTs = region.updatedTs,
orders = new List<OrderModel>(region.orders.Count)
};
var json = File.ReadAllText("/test.json");
var otherBotOrders = JsonSerializer.Deserialize<OrdersTimesModel>(json);
OtherBotOrders = new Dictionary<string, OrderTimesInfoModel>();
foreach (var otherBotOrder in otherBotOrders.OrdersTimesInfo)
{
//OtherBotOrders.Add(otherBotOrder.Id, otherBotOrder);
BotController.WriteLine($"{otherBotOrder.Id}"); //Output ID orders to the console works
}
foreach (var order in region.orders)
{
if (ConvertToDecimal(order.price) < 1 || !byOurOrders)
{
int i = 0;
var isOurOrder = false;
while (i < ourOrders.Count && !isOurOrder)
{
if (ourOrders[i].id.Equals(order.id, StringComparison.InvariantCultureIgnoreCase))
{
isOurOrder = true;
}
++i;
}
if (!isOurOrder)
{
result.orders.Add(order);
}
}
}
return result;
}
OrdersTimesModel Looks like that:
public class OrdersTimesModel
{
public List<OrderTimesInfoModel> OrdersTimesInfo { get; set; }
}
test.json:
{"OrdersTimesInfo":[{"Id":"1"},{"Id":"2"}]}
Added:
I'll try to clarify the question:
There are three lists with ID:
First (all orders): region.orders, as order.id
Second (our orders): ourOrders, as ourOrders[i].id in a while loop
Third (our orders 2): from the /test.json file, as an array {"Orders":[{"Id":"12345..."...},{"Id":"12345..." ...}...]}
There is a foreach in which there is a while, where the First (all orders) list and the Second (our orders) list are compared. If the id's match, then these are our orders: isOurOrder = true;
Accordingly, those orders that isOurOrder = false; will be added to the result: result.orders.Add(order)
I need:
So that if (ourOrders[i].id.Equals(order.id, StringComparison.InvariantCultureIgnoreCase)) would include more Id's from the Third (our orders 2) list.
Or any other way to do it?
You should be able to completely avoid writing loops if you use LINQ (there will be loops running in the background, but it's way easier to read)
You can access some documentation here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/introduction-to-linq-queries
and you have some pretty cool extension methods for arrays: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=net-6.0 (these are great to get your code easy to read)
Solution
unsing System.Linq;
private RegionModel FilterByOurOrders(RegionModel region, List<OurOrderModel> ourOrders, MarketSettings market, bool byOurOrders)
{
var result = new RegionModel
{
updatedTs = region.updatedTs,
orders = new List<OrderModel>(region.orders.Count)
};
var json = File.ReadAllText("/test.json");
var otherBotOrders = JsonSerializer.Deserialize<OrdersTimesModel>(json);
// This line should get you an array containing
// JUST the ids in the JSON file
var idsFromJsonFile = otherBotOrders.Select(x => x.Id);
// Here you'll get an array with the ids for your orders
var idsFromOurOrders = ourOrders.Select(x => x.id);
// Union will only take unique values,
// so you avoid repetition.
var mergedArrays = idsFromJsonFile.Union(idsFromOurOrders);
// Now we just need to query the region orders
// We'll get every element that has an id contained in the arrays we created earlier
var filteredRegionOrders = region.orders.Where(x => !mergedArrays.Contains(x.id));
result.orders.AddRange(filteredRegionOrders );
return result;
}
You can add conditions to any of those actions (like checking for order price or the boolean flag you get as a parameter), and of course you can do it without assigning so many variables, I did it that way just to make it easier to explain.

In a C# List how to select all columns + several custom columns, so that in each row all columns are flat (not nested)?

I have a simple List that each row of it has 50 columns. I want return all 50 columns + 3 custom columns but i want make each row of the list like a flat (not nested) object.
Example:
var newList = list.Select(x => new
{ x,
d.CustomColA = x.ColA+10,
d.CustomColB = x.ColB+30,
d.CustomColC = x.ColC+50
});
Result: It works well but each result row is like an nested object:
var row = newList.FirstOrDefault();
row.x.ColA
row.x.ColB
row.x.ColC
.....
row.CustomColA
row.CustomColB
row.CustomColB
Expected Result:
var row = newList.FirstOrDefault();
row.ColA
row.ColB
row.ColC
.....
row.CustomColA
row.CustomColB
row.CustomColB
I used dynamic type and wrote the following code but it did not return expected result:
var newList = list.Select(x =>
{
dynamic d = x;
d.CustomColA = x.ColA+10;
d.CustomColB = x.ColB+30;
d.CustomColC = x.ColC+50;
return d;
//return x;
});
Result in watch panel: 'newList.FirstOrDefault()' threw an exception of type 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException'
Update:
Attention: I wrote in my question that i have 50 columns and wrote an
example to show you i do not want name all the columns in the Select!
(I know I can write name of all 53 column in a Select!) So that is not the correct answer.
Attention2: In the real project i have complicated custom columns but i wrote very simple example here to show what i want. Please write your flexible answers. Thank you.
So what you are looking to do is basically map a set of properties. There are libraries for this sort of thing, Automapper is a good one. However, you can also accomplish this yourself with a reflective constructor in an inheriting class. That would look like this:
Assumptions:
Your class with 50 columns is called Cols
You can inherit from Cols
public class CustomColumns : Cols
{
public int CustomColA {
get{
return this.ColA + 10;
}
}
public int CustomColB {
get{
return this.ColB + 30;
}
}
public int CustomColC {
get{
return this.ColC + 50;
}
}
public CustomColumns(Cols cols)
{
string[] localNames = this.GetType().GetMembers().Where(m => m.MemberType == MemberTypes.Property).Select(m => m.Name).ToArray();
string[] ctorNames = cols.GetType().GetMembers().Where(m => m.MemberType == MemberTypes.Property).Select(m => m.Name).ToArray();
string[] names = localNames.Intersect(ctorNames).ToArray();
foreach (string s in names)
{
PropertyInfo propSet = this.GetType().GetProperty(s);
PropertyInfo propGet = typeof(Cols).GetProperty(s);
propSet.SetValue(this, propGet.GetValue(cols, null));
}
}
}
Here is a demo of this working in dotnetfiddle: https://dotnetfiddle.net/AKPYQD
Use an anonymous type within the Select:
var newList = list.Select(x =>
new
{
ColA = x.ColA,
ColB = x.ColB,
ColC = x.ColC,
CustomColA = x.ColA+10;
CustomColB = x.ColB+30;
CustomColC = x.ColC+50;
}).ToList();
The .Select() is creating a new object. Even if you just wrote:
var newList = list.Select(x => new
{ x });
You've get a nested property. You are just going to have to explicitly assign every column to a new property (Do you really need all 50 of them?)

LINQ - Deserialize JSON column and filter

How to deserialize/serialize a property with JSON string array value and then filter (using where clause) in LINQ inside a lambda expression?
void Main()
{
var regionList = new List<Row>() {
new Row { RegionJsonList = "[\"QLD\",\"NSW\"]" },
new Row { RegionJsonList = "[\"TAZ\",\"SA\"]" },
new Row { RegionJsonList = "[\"QLD\",\"VIC\"]" }
};
var filterRegionList = new List<string>() {
"QLD", "NSW"
};
var queryable = regionList.AsQueryable();
// this is obviously wrong, i just want to find the Row that contains one on filterRegionList
var result = queryable.Where(r => JsonConvert.DeserializeObject<string[]>(r.RegionJsonList).Contains(filterRegionList));
result.Count().Dump(); // should be 2
}
class Row
{
public string RegionJsonList { get;set; }
}
Following would work:
var result =
filterRegionList.Aggregate(regionList,(current,filter) =>
current.Where( r => r.RegionJsonList.Contains(filter)).ToList())
Aggregating the filterRegionList and regionList and thus applying filters for the final result. I did not find a requirement to Deserialize the RegionJsonList, since this would work as is, but you may add that part in case you are keen.
Also we are applying And filter via aggregation, it checks for the rows which contains both the filters, and thus provide the result, you may modify filter to achieve more number of rows, like following will select two entries from original regionList
var filterRegionList = new List<string>() { "QLD" };
To filter for rows that contain at least one of the entries from filterRegionList, you can use Enumerable.Intersect and check for non-empty intersections:
var resultAny = queryable.Where(r => JsonConvert.DeserializeObject<string[]>(r.RegionJsonList).Intersect(filterRegionList).Any());
To filter for rows that contain all of the entries from filterRegionList, you can use Enumerable.Except to remove the row's entries from the filter list. If everything gets removed, it's a match:
var resultAll = queryable.Where(r => !filterRegionList.Except(JsonConvert.DeserializeObject<string[]>(r.RegionJsonList)).Any());
(It wasn't entirely clear from your question which you wanted.)

C#: LINQ query with split and parsing

I have an object with a String field containing a comma separated list of integers in it. I'm trying to use LINQ to retrieve the ones that have a specific number in the list.
Here's my approach
from p in source
where (p.Keywords.Split(',').something.Contains(val))
select p;
Where p.Keywords is the field to split.
I've seen the following in the net but just doesn't compile:
from p in source
where (p.Keywords.Split(',').Select(x=>x.Trim()).Contains(val))
select p;
I'm a LINQ newbie, but had success with simpler queries.
Update:
Looks like I was missing some details:
source is a List containing the object with the field Keywords with strings like 1,2,4,7
Error I get is about x not being defined.
Here's an example of selecting numbers that are greater than 3:
string str = "1,2,3,4,5,6,7,8";
var numbers = str.Split(',').Select(int.Parse).Where(num => num > 3); // 4,5,6,7,8
If you have a list then change the Where clause:
string str = "1,2,3,4,5,6,7,8";
List<int> relevantNums = new List<int>{5,6,7};
var numbers = str.Split(',').Select(int.Parse).Where(num => relevantNums.Contains(num)); // 5,6,7
If you are not looking for number but for strings then:
string str = "1,2,3,4,5,6,7,8";
List<string> relevantNumsStr = new List<string>{"5","6","7"};
var numbers = str.Split(',').Where(numStr => relevantNumsStr.Contains(numStr)); // 5,6,7
Here is an example of how you can achieve this. For simplicity I did to string on the number to check for, but you get the point.
// class to mimic what you structure
public class MyObj
{
public string MyStr{get;set;}
}
//method
void Method()
{
var myObj = new List <MyObj>
{
new MyObj{ MyStr="1,2,3,4,5"},
new MyObj{ MyStr="9,2,3,4,5"}
};
var num =9;
var searchResults = from obj in myObj
where !string.IsNullOrEmpty(obj.MyStr) &&
obj.MyStr.Split(new []{','})
.Contains(num.ToString())
select obj;
foreach(var item in searchResults)
Console.WriteLine(item.MyStr);
}
Thanks for all the answers, although not in the right language they led me to the answer:
from p in source where (p.Keywords.Split(',').Contains(val.ToString())) select p;
Where val is the number I'm looking for.

Specifying return rows in LINQ2DataSet

I have a requirement to extract a distinct subset of rows from a DataTable, and thought LINQ2DataSets may be a useful and clean way to do this, however it appears that it is not possible to simply identify return rows from a LINQ2DS query as follows
var result = from r in fips.AsEnumerable() select
r.Field<string>("FACILITY_PROCESS_SUB_GROUP_CODE"),
r.Field<string>("PROCESS_SUB_GROUP_NAME"),
r.Field<string>("...
as I start getting errors after the first comma.
Is this a correct assumption, and how would I get around it to return a subset of columns from the dataset that I can apply a Distinct() method to?
You forgot the new statement and field names:
var result = from r
in fips.AsEnumerable()
select new
{
FacProcess = r.Field<string>("FACILITY_PROCESS_SUB_GROUP_CODE"),
GroupName = r.Field<string>("PROCESS_SUB_GROUP_NAME"),
Item3 = r.Field<string>("Item3")
};
You can also explicitly declare that you are going to use a type:
var result = from r
in fips.AsEnumerable()
select new MyType("InitClassParams")
{
FacProcess = r.Field<string>("FACILITY_PROCESS_SUB_GROUP_CODE"),
GroupName = r.Field<string>("PROCESS_SUB_GROUP_NAME"),
Item3 = r.Field<string>("Item3")
};
Scott Guthrie (VP Developer Devision, Microsoft) has some good info about LINQ (he talks about LINQ to SQL, but most of it applies regardless).
Then apply the distinct clause:
var result = from r
in fips.AsEnumerable()
select new
{
FacProcess = r.Field<string>("FACILITY_PROCESS_SUB_GROUP_CODE"),
GroupName = r.Field<string>("PROCESS_SUB_GROUP_NAME"),
Item3 = r.Field<string>("Item3")
}
distinct;
Then put it to a list or iterate over it. Nothing will be selected/distincted/etc until something like on of the following is run:
var list = result.ToList()
foreach(var item in result) {}

Categories

Resources