This is a way of filtering an IQueryable using Dynamic Linq. It composes strings based on Strings retrieved from the database and static values and functions which return strings stored in a dictionary with a key of a columnName for the filter Criterion
Each filter criterion has on it an appColumn which contains the lookup name for the path of the properties on a queryable which it needs to filter on.
public class FuncVal<TC>
{
public Func<TC, string> Func;
public String Val;
}
This is the structure of the class inside of the valueDictionary.
It can either return a static string, or it can return a function which takes a controller to be used for finding variables like user name, session variables or anything that needs to be calculated. It too must return a string and can be crated as a lambda
This function is a static function on FilterExpressionComposer. It takes 5 arguments and returns a single filter expression string usable by Dynamic linq.
Let's take a real world example from inside a PatientController
public class PatientController : Controller{
public ActionResult Index(){
var colName = "Acuities";
var express = ".Any(,#{=|Serious,Critical}";
var pathDict = new Dictionary<String, String>
{
{"Acuities", "Patient.Reports[]Condition.Trim()"}
};
var valueDict = new Dictionary<String, FuncVal<PatientsController>>
{
{"Serious", new FuncVal<PatientsController>(){Val = "\"Serious\""}},
{"Critical", new FuncVal<PatientsController>(){Val = "\"Critical\""}}
};
FilterExpressionComposer.FilterExpressions(colName,express,this,pathDict,valueDict);
}
}
Let's take a look at the Acuities value in pathDict Patient.Reports[]Condition.Trim()
This is a path to a condition property inside of some object. That object has a property patient. Patient has an array of Reports, and each Report has a property Condition. We need to tell the function that Reports is an Array, so that it knows where to put the operators inside of express. The operands are held inside of .Any(,#{=|Serious,Critical}
Which first parses out any value containers #{=|Serious,Critical}
and calls .Split(',')
. This creates two arrays of values and operators.
The function will call .Split("[]")
and place the operators in order of appearence in the operators array. => Patient.Reports.Any(
Then it will use the next string from the .Split("[]")
array => Patient.Reports.Any(Condition.Trim()
The function see's that there are no more operands so it appends the next String in the value array and closes any unclosed parenthesis. => Patient.Reports.Any(Condition.Trim()#{=|Serious,Critical})
The function will parse out exactly how to compare each of the values. We get the comparision operator from the characters before |
, which in this case is =
. It then calls .Split(',')
which will give us two keys Serious
and Critical
to lookup values on. So it then finds the string values for those keys in the expressions dictionary. => \"Serious\"
and => \"Critical\"
It then looks at where the #{=|Serious,Critical}
value is in the string using the, finds the previous operand, which in this case is .Any(
and copies the string inbetween the end of the operand and the end of the value with two comparisons => Condition.Trim()#{=|Serious,Critical}
It then copies that string for each value found and replaces #{=|Serious,Critical}
with the found value from the coresponding FuncVal. => Condition.Trim()="Serious"
And => Condition.Trim()="Critical"
It joins those on an Or
and replaces Condition.Trim()#{=|Serious,Critical}
with Condition.Trim()="Serious" Or Condition.Trim()="Critical"
resulting in => Patient.Reports.Any(Condition.Trim()="Serious" Or Condition.Trim()="Critical")
We can now use this string on a queryable using dynamic linq
var filterExpression = FilterExpressionComposer.FilterExpressions(colName,express,this,pathDict,valueDict);
someQueryable.Where(filterExpression);
Note: the Trim()
call is considered a modification property of the Condition property, so it's not an operand
FilterExpressionComposer is also able to compare more than one property to different values inside of some array.
First construct our values and dictionaries.
public class PatientController : Controller{
public ActionResult Index(){
var colName = "PatientSelfAndType";
var express = ".Any(,#{=|CurrentUserPaceartID}, And ,#{=|AttendingType}";
var pathDict = new Dictionary<String, String>
{
{"PatientSelfAndType", "Patient.PhysicianPatients[]Physician.PhysicianUser.User.Username[]PhysicianPatientType.Type"}
};
var valueDict = new Dictionary<String, FuncVal<PatientsController>>
{
{"CurrentUserPaceartID", new FuncVal<PatientsController>(){Func = ((controller)=> GetCurrentUserPaceartID(controller)) }},
{"AttendingType", new FuncVal<PatientsController>(){Val = "2"}}
};
FilterExpressionComposer.FilterExpressions(colName,express,this,pathDict,valueDict);
}
public static String GetCurrentUserPaceartID(PatientsController controller)
{
return "\"" + controller.User.Identity.Name + "\"";
}
}
Again we split the path on []
and construct the start of our filter string with the first operand we find and go deeper into the object => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username
Then we come to an And
operand in express. The way this works is if the operand doesn't start with .
then the function will use the first value from the filter expression, and then place the current opperator => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username#{=|CurrentUserPaceartID} And
Notice, the And
has two buffer spaces around it. if the operand has no .
then it's necessary to add this buffer otherwise the string will become part of the propertyname and likely throw a no property found on object exception.
Then it will use the next String from the .Split("[]")
array => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username#{=|CurrentUserPaceartID} And PhysicianPatientType.Type
Because the And
Didn't create a new context as the .Any(
would did, we can see that PhysicianPatientType
is still refering back to PhysicianPatients
So we can filter based on that same object, makeing both conditions necessary for fulfilling the Any on PhysicianPatients
. Now the function will use the next Value to compare. => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username#{=|CurrentUserPaceartID} And PhysicianPatientType.Type#{=|AttendingType}
The last operand has been used so it will close out any unclosed parenthesis => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username#{=|CurrentUserPaceartID} And PhysicianPatientType.Type#{=|AttendingType})
Note: We could have achieved the same effect by replacing the previous .Any(
with a .Where(
and the And
with ).Any(
. This would have output => Patient.PhysicianPatients.Where(Physician.PhysicianUser.User.Username#{=|CurrentUserPaceartID}).Any(PhysicianPatientType.Type#{=|AttendingType})
This however creates two separate Queries instead of combining them.
It then finds each value and replaces the value string with the value gotten from the valueDict. This one is slightly different. We see that the first FuncVal contains a lambda. This get's passed the context object (in this case the current PatientController) and returns some String, (in this case the current user's username). The third parameter and the gerneric Type passed into the FuncVal must match. The other Value is a simple int. => Patient.PhysicianPatients.Any(Physician.PhysicianUser.User.Username="SomeUserName" And PhysicianPatientType.Type=1