Reputation: 57
I am trying to filter out rows in a table.
I have following code:
if (groupRadio.Checked)
{
using (gf = new GFilterEntities())
{
dgvFiltered.DataSource = gf.GroupTs.Where(x => x.G_Platform.Equals(platformCombo.Text) &&
x.G_Type.Equals(typeCombo.Text) && x.fieldNameCombo.Text <= how to do this
}
}
I want to dynamically pass a column name from a ComboBox in x.fieldNameCombo.Text but am unable to do so. How can this be achieved?
Example: G_Platform and G_Type are column names in the GroupT table. When I enter x. I can see these values populate in Visual Studio.
However, I'd like to be able to use a value that user inputs in a combo box for filtering purposes. So something like x.user value in fieldNameComboBox.Equals(textBox1.text). The fieldNameComboBox value WILL be a column name, just like G_Platform and G_Type.
Thanks.
UPDATE Based on one of the comments below (by sam), my code now reads:
private void btnSearch_Click(object sender, EventArgs e)
{
if (groupRadio.Checked)
{
using (gf = new GFilterEntities())
{
var testClass = new TestClass { ComboBoxSelectedText = fieldNameCombo.Text, UserInputValue = txt1.Text};
var comboBoxSelectedColumnValues = from object item in gf.GroupTs
let propertyInfo = item.GetType().GetProperties()
from info in propertyInfo
where string.Equals(info.Name, testClass.ComboBoxSelectedText)
select (string)info.GetValue(item)
into s
where !string.IsNullOrWhiteSpace(s)
select s;
dgvFiltered.DataSource = gf.GroupTs.Where(x => x.G_Platform.Equals(platformCombo.Text) &&
x.G_Type.Equals(typeCombo.Text) && comboBoxSelectedColumnValues.Equals(testClass.UserInputValue)
).ToList();
}
}
}
public class TestClass
{
public string ComboBoxSelectedText { get; set; }
public string UserInputValue { get; set; }
}
I am getting an error that says: System.NotSupportedException: 'Unable to cast the type 'Filtering.GroupT' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.'
Upvotes: 0
Views: 3824
Reputation: 57
Thank you so much for your responses. I found the following solution to be the most optimal:
I created a few static helper methods that each returned a Lambda Expression. For example, for ".Contains" I added the following method:
private static Expression<Func<T, bool>> GetContainsExpression<T>(string propertyName, string propertyValue)
{
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
For .StartsWith the following method (similarly):
private static Expression<Func<T, bool>> GetStartsWithExpression<T>(string propertyName, string propertyValue)
{
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
And the same goes to ".Equals" and ".EndsWith".
Later on, in my click method above (in my question), I added the following lambda expressions: (This one is for .Contains)
if (valCombo.Text.Equals("contains"))
{
dgvFiltered.DataSource = af.Accounts.Where(GetContainsExpression<Account>(fieldNameCombo.Text, txt1.Text))
.Where(x => x.A_Platform.Equals(platformCombo.Text) && x.A_Type.Equals(typeCombo.Text)).ToList();
}
This one is for .StartsWith:
if (valCombo.Text.Equals("starts with"))
{
dgvFiltered.DataSource = af.Accounts.Where(GetStartsWithExpression<Account>(fieldNameCombo.Text, txt1.Text))
.Where(x => x.A_Platform.Equals(platformCombo.Text) && x.A_Type.Equals(typeCombo.Text)).ToList();
}
And so on. Yes, it does make both the .Wheres in each code sample above an && expression. So, to tackle the || case where one Where is true and the other is not, I added the following if statement above them:
if (fieldNameCombo.Text == "")
{
dgvFiltered.DataSource = af.Accounts.Where(x => x.A_Platform.Equals(platformCombo.Text) && x.A_Type.Equals(typeCombo.Text)).ToList();
}
Hope this provides clarity on this issue. Let me know if you have any questions.
Upvotes: 1
Reputation: 120
When we need something "dynamic" in the query like this that LINQ doesn't really support we use the "Raw SQL Query" feature of EF Core that is explained here: https://learn.microsoft.com/en-us/ef/core/querying/raw-sql
So you could write something like:
var whereStatement = $"G_Platform = '{platformCombo.Text}' AND {fieldNameCombo.Text} = '{textBox1.text}'"; // generate this based on your user's input
dgvFiltered.DataSource = gf.GroupTs
.FromSqlRaw($"SELECT * FROM GroupTs WHERE {whereStatement}")
.ToList();
Couple of notes:
When introducing any user-provided values into a raw SQL query, care must be taken to avoid SQL injection attacks. In addition to validating that such values don't contain invalid characters, always use parameterization which sends the values separate from the SQL text.
In particular, never pass a concatenated or interpolated string ($"") with non-validated user-provided values into FromSqlRaw or ExecuteSqlRaw. The FromSqlInterpolated and ExecuteSqlInterpolated methods allow using string interpolation syntax in a way that protects against SQL injection attacks.
Upvotes: 3
Reputation: 1893
Like ajphall mentioned depending on your version of EF you can use FromSqlRaw for EF Core or FromSql for older versions, in your case please use FromSqlInterpolated or sql parameters to prevent sql injection documentation: https://learn.microsoft.com/en-us/ef/core/querying/raw-sql
To solve your query issues you can do something like this:
var plataformParameter = platformCombo.Text;
var typeParameter = typeCombo.Text
var fieldParameter = fieldNameCombo.Text;
dgvFiltered.DataSource = gf.GroupTs
.FromSqlInterpolated(
$"SELECT * FROM GroupTs WHERE
G_Platform = {plataformParameter } AND
G_Type = {typeParameter} AND
(G_Name = {fieldParameter}
OR G_Owner = {fieldParameter}")
.ToList()
Depending on your data if you don't want to return nulls you could check for that first.
...((G_Name<>null and G_Name={fieldParameter})or (G_Owner <> null and G_Owner={fieldParameter}))
Hope this helps.
UPDATE:
You can use dynamic SQL but be aware that you must validate the inputs or you will endup with sql injection:
Call this stored procedure form your code and pass the parameters you need.
--This is just and example I haven't run this query.
DECLARE @platform VARCHAR(20)
DECLARE @columnName VARCHAR(20) -- this should be the name from the input
DECLARE @columnValue VARCHAR(20)--
DECLARE @typeValue VARCHAR(20)
DECLARE @sqlCommand VARCHAR(255)
--DO SOME CHECKS TO ENSURE THE COLUMN ACTUALLY EXISTS.
--DO NOT JUST RUN WHAT YOU GOT FROM THE INPUT
SET @sqlCommand = 'SELECT * FROM GroupTs WHERE G_PLATFORM=' + @platform +'AND G_Type='+@typeValue+'AND'+@columnName+'='+@columnValue
EXEC (@sqlCommand)
Here is more info about this https://www.essentialsql.com/build-dynamic-sql-stored-procedure/
Upvotes: 0