I need to order a collection of data dynamically. I have this working at the top level:
//where prop is a string passed in, ex: "ShoeSize"
_clowns
.AsEnumerable()
.OrderBy(x => x.GetType().GetProperty(prop)?.GetValue(x))
.Select(x => x.Id)
.ToList();
And that works so long as I only need to order by some property of Clowns. But what if I need to order Clowns by a property of their Car? I think I'm close, but can't clear the gap:
//for prop = "Car.ClownCapcity"
var queryBuilder = _clowns;
var orderProp = prop;
if (prop.Contains(".")){
string[] props = prop.Split(".");
foreach(string oneProp in props){
if (props.Last() != oneProp){
//this line is wrong for sure
queryBuilder.Include(x => x.GetType().GetProperty(oneProp));
} else {
orderProp = oneProp;
}
}
}
queryBuilder.AsEnumerable()
.OrderBy(x => x.GetType().GetProperty(orderProp)?.GetValue(x))
.Select(x => x.Id)
.ToList();
This doesn't work because I cannot build up queryBuilder, reassigning at the Include doesn't work because the return type is different. I also haven't figured out how to dynamically go deeper inside the final OrderBy.
Is there a decent way to do this in Linq, or should I go build a SQL string?
There are security concerns, those can be handled elsewhere and aren't terribly relevant to this question
Update
Progress! Got it to work two-levels deep, but only when I explicitly know it's two-levels deep. I haven't figured out the abstraction yet.
if (prop.Contains(".")){
string[] props = prop.Split(".");
string includeProp = props.FirstOrDefault();
string orderProp = props.LastOrDefault();
return _Clowns
.Include(includeProp)
.AsEnumerable()
.OrderBy(x => {
var y = x.GetType().GetProperty(formattedProp)?.GetValue(x);
return y?.GetType().GetProperty(orderProp)?.GetValue(y);
})
.Select(x => x.Id)
.ToList();
}
Answering with my own solution. Please give me a better answer if I'm way over-complicating this.
if (prop.Contains(".")){
string[] props = prop.Split(".");
string includePath = prop.remove(prop.LastIndexOf("."));
IQueryable<ClownModel> queryBuilder = _clowns.Include(includePath);
return queryBuilder.
.AsEnumerable()
.OrderBy(x => {
object y = x;
foreach (var prop in props)
{
//avoid unnecessary depth checks if a null is found
if (y == null)
{
break;
}
//if we're dealing with a collection
if (y?.GetType().Namespace == "System.Collections.Generic")
{
//gets a HashMap<ModelType>
var o = (dynamic)y;
//can't access this HashMap with [0] or .First() though?
foreach(object m in o)
{
//cheat and iterate
y = m;
break;
}
}
y = y?.GetType().GetProperty(prop)?.GetValue(y);
}
return y;
})
.Select(x => x.Id)
.ToList();
}
My initial workup was wrong, it was still only working for 2-level deep. This works for any levels, and accounts for collections of properties along the way. Intriguingly, the Include function can take a dot-notation string (super handy), but not the OrderBy.
So pass in the Include, then loop through each property level inside the OrderBy, taking the first record if it's a collection.
Bing-bam-boom, you got a baby pachyderm. Back to you, Trent.
Related
I call dynamically sort rows of a table when the orderby column is in the parent table doing the following...
public List<ServiceRequest> SortSRsByParentFields(string p_Criteria,
bool p_sortDescending,
bool p_ShowAll = true) {
var propertyInfo = typeof(ServiceRequest).GetProperty(p_Criteria);
var sortedList1 = new List<ServiceRequest>();
var sortedList2 = new List<ServiceRequest>();
var myServiceRequests = GetMyServiceRequests();
var otherServiceRequests = GetOthersServiceRequests();
if (p_sortDescending)
{
sortedList1 = myServiceRequests
.AsEnumerable()
.OrderByDescending(x => propertyInfo.GetValue(x, null)).ToList();
sortedList2 = otherServiceRequests.AsEnumerable()
.OrderByDescending(x => propertyInfo.GetValue(x, null))
.ThenBy(x => x.Client.LastNameFirst).ToList();
}
else
{
sortedList1 = myServiceRequests.AsEnumerable()
.OrderBy(x => propertyInfo.GetValue(x, null)).ToList();
sortedList2 = otherServiceRequests.AsEnumerable()
.OrderBy(x => propertyInfo.GetValue(x, null))
.ThenBy(x => x.Client.LastNameFirst).ToList();
}
var allSRs = p_ShowAll == false ? sortedList1.Concat(sortedList2).Take(1000)
.ToList() : sortedList1.Concat(sortedList2).ToList();
return allSRs;
}
But I can't seem to make this method work if the orderby column is in a child table (a table related to the parent though an FKey).
So the question is how do I make that work?
EF isn't really designed with dynamic sorting in mind. But there are alternatives you can use for cases like this without replacing the rest of your EF code.
For example, with Tortuga Chain you can write:
ds.From("ServiceRequests", [filter]).WithSorting (new SortExpression(p_Criteria, p_sortDescending)).ToCollection<ServiceRequest>().Execute();
You can also just generate SQL directly, but I don't recommend that approach because you have to carefully check the sort expression to ensure it is actually a column name and not a SQL injection attack.
There must be a way to compare two sets of results while staying in LINQ. Here's my existing code that uses a HashSet to do the comparison after two separate queries:
public static void AssertDealershipsShareTransactionGatewayCredentialIds(long DealershipLocationId1,
long DealershipLocationId2)
{
using (var sqlDatabase = new SqlDatabaseConnection())
{
var DealershipCredentials1 =
sqlDatabase.Tables.DealershipLocationTransactionGateway
.Where(x => x.DealershipLocationId == DealershipLocationId1)
.Select(x => x.TransactionGatewayCredentialId);
var DealershipCredentials2 =
sqlDatabase.Tables.DealershipLocationTransactionGateway
.Where(x => x.DealershipLocationId == DealershipLocationId2)
.Select(x => x.TransactionGatewayCredentialId);
var doSetsOfCredentialsMatch = new HashSet<int>(DealershipCredentials1).SetEquals(DealershipCredentials2);
Assert.IsTrue(doSetsOfCredentialsMatch,
"The sets of TransactionGatewayCredentialIds belonging to each Dealership did not match");
}
}
Ideas? Thanks.
Easy answer (This will make 1, possibly 2 database calls, both of which only return a boolean):
if (list1.Except(list2).Any() || list2.Except(list1).Any())
{
... They did not match ...
}
Better answer (This will make 1 database call returning a boolean):
var DealershipCredentials1 =
sqlDatabase.Tables.DealershipLocationTransactionGateway
.Where(x => x.DealershipLocationId == DealershipLocationId1)
.Select(x => x.TransactionGatewayCredentialId);
var DealershipCredentials2 =
sqlDatabase.Tables.DealershipLocationTransactionGateway
.Where(x => x.DealershipLocationId == DealershipLocationId2)
.Select(x => x.TransactionGatewayCredentialId);
if (DealershipCredentials1.GroupJoin(DealershipCredential2,a=>a,b=>b,(a,b)=>!b.Any())
.Union(
DealershipCredentials2.GroupJoin(DealershipCredential1,a=>a,b=>b,(a,b)=>!b.Any())
).Any(a=>a))
{
... They did not match ...
}
The second method works by unioning a left outer join that returns a boolean indicating if any unmatching records were found with a right outer join that does the same. I haven't tested it, but in theory, it should return a simple boolean from the database.
Another approach, which is essentially the same as the first, but wrapped in a single LINQ, so it will always only make 1 database call:
if (list1.Except(list2).Union(list2.Except(list1)).Any())
{
}
And another approach:
var common=list1.Intersect(list2);
if (list1.Except(common).Union(list2.Except(common)).Any()) {}
I would like to use linq to check if one of multiple logic statements return true.
So far I've tried the following:
winningCombinations = new[,]
{
{1,2,3},
{4,5,6},
{7,8,9},
{1,4,7},
{1,4,7},
{3,6,9},
{1,5,9},
{3,5,7}
};
if (Enumerable.Range(0,7).Where(x => this.contentOf(winningCombinations[x,0]) == this.contentOf(winningCombinations[x,1]) &&
this.contentOf(winningCombinations[x,1]) == this.contentOf(winningCombinations[x,2]))
.Select(x => true).FirstOrDefault(x => x))
{
_isDone = true;
_game.wonBy(_turnOf);
}
Basically contentOf() require an Index as a parameter and return a char value.
My question is, is there a way to make my code work? I'd like to use the coordinates contained in "winningCombinations" to check if there is a winningCombination on my grid (3 identical characters), preferably using Linq.
I'm fairly unclear on what you're trying for, but it sounds like IEnumerable.Any should do what you want.
if (Enumerable.Range(0,7)
.Any(x => this.contentOf(winningCombinations[x,0]) == this.contentOf(winningCombinations[x,1]) &&
this.contentOf(winningCombinations[x,1]) == this.contentOf(winningCombinations[x,2]))
{
_isDone = true;
_game.wonBy(_turnOf);
}
This is pretty self-explanatory, but essentially the goal is to find any sets for which the result it true. If it finds one, it returns true and jumps into the if block. If it looks through the full array and doesn't find any matches, it will return false.
If you're looking for even more LINQ, and who isn't, this is arguably cleaner. Arguably.
if (Enumerable.Range(0, winningCombinations.GetLength(0))
.Select(c => Enumerable.Range(0, winningCombinations.GetLength(1)
.Select(x => this.contentOf(winningCombinations[c, x])
.Any(x => !x.Distinct().Skip(1).Any())
{
_isDone = true;
_game.wonBy(_turnOf);
}
Broken up a bit,
// Get the indexes for of the array
var ys = Enumerable.Range(0, winningCombinations.GetLength(0));
var xs = Enumerable.Range(0, winningCombinations.GetLength(1));
// Select the values at each index, becomes a collection of collections
var vals = ys.Select(y => xs.Select(x => this.contentOf(winningCombinations[y, x]));
// Discover whether at least one of the sets has exactly one value contained.
return vals.Any(c => !x.Distinct().Skip(1).Any())
Edit: Code works fine, it was an other bug.
I had comment out the //department.IdAgency = reader.GetByte(2); line, in the created departmentList. When I removed the // then the IQueryable<string> with .Where works fine. Sorry for the inconvenience!
static List<Department> CreateDepartmentList(IDataReader reader)
{
List<Department> departmentList = new List<Department>();
Department department = null;
while (reader.Read())
{
department = new Department();
department.Id = reader.GetByte(0);
department.Name = reader.GetString(1);
//department.IdAgency = reader.GetByte(2);
if (!reader.IsDBNull(3))
{ department.IdTalkGroup = reader.GetInt16(3); }
departmentList.Add(department);
}
return departmentList;
}
Original question:
I have an IQueryable<string> query, that works. But how do I use .Where?
IQueryable<string> query = departmentList.AsQueryable()
.OrderBy(x => x.Name)
.Select(x => x.Name);
I have tried this, but it does not work:
IQueryable<string> query = departmentList.AsQueryable()
.OrderBy(x => x.Name)
.Where(x => x.IdAgency == idAgencySelected[0])
.Select(x => x.Name);
All the .Where() call does is apply a filtering method to each element on the list, thus returning a new IEnumerable.
So, for some IQueryable<string>...
IEnumerable<string> results = SomeStringList.Where(s => s.Contains("Department"));
...You would get a list of strings that contain the word department.
In other words, by passing it some boolean condition that can be applied to a member of the queryable collection, you get a subset of the original collection.
The reason your second block of code does not work, is because you're calling a method or property that does not belong to string. You may want to consider querying against the more complex type, if it has identifier data, and then take the names of the elements and add them to some list instead.
About the homework:
There are casters(witch(0)/fairy(1)) and they have spellpower(int). I stored them in a list.
I'm to find the best of both types. (There can be multiple casters with the same spellpower)
I've come up with this code, but there is a problem. If the caster with the most spellpower is a 1, then the first FindAll won't return anything, because it tries to find the caster with type 0 AND with the most spellpower. How can I get a list containing type 0 caster(s) with the most spellpower, if the caster with the most overall spellpower is type 1?
private List<Caster> BestCasters()
{
List<Caster> temp = new List<Caster>();
temp = casters.FindAll(x => x.SpellPower == casters.Max(y => y.SpellPower) && (x.TypeOfCaster == 0));
temp.AddRange(casters.FindAll(x => x.SpellPower == casters.Max(y => y.SpellPower) && (x.TypeOfCaster == 1)));
temp.OrderBy(x => x.TypeOfCaster).ThenBy(y => y.CasterName);
return temp;
}
The LINQ GroupBy behavior is perfect for this:
var strongest_casters = casters.GroupBy(c => c.TypeOfCaster)
.Select(grp => grp.OrderByDescending(x => x.SpellPower)
.First()
);
Or to return more than one of each type:
var strongest_casters = casters.GroupBy(c => c.TypeOfCaster)
.SelectMany(grp => grp.Where(y.SpellPower == grp.Max(x => x.SpellPower))
);
private List<Caster> BestCasters()
{
var witches = casters.Where(x => x.TypeOfCaster == 0).ToList();
var fairies = casters.Where(x => x.TypeOfCaster == 1).ToList();
int witchesMax = witches.Max(x => x.SpellPower);
int fairiesMax = fairies.Max(x => x.SpellPower);
var temp = witches.Where(x => x.SpellPower == witchesMax).ToList();
temp.AddRange(fairies.Where(x => x.SpellPower == fairiesMax));
return temp.OrderBy(x => x.TypeOfCaster).ThenBy(y => y.CasterName).ToList();
}
If you have to use FindAll like this you should invoke the Max on a subset only containing the casters of the right kind. Of course it would make more sense to split the initial list first and then fetch the strongest caster of each kind.
Since you did not tell what exactly you have to do I can only hope that you are allowed to split :-)