iT邦幫忙

1

C# LINQ WHERE條件使用外部變數

public ActionResult test(string a="", string search = "")
{
...
var r = from c in db.n
select new {
c.id,
c.projectname,
c.type,
c.name};

if (search!="")
{
r = r.Where(x => x.a.Contains(search));
}
...
}
以上為預期想要的目標
可是實際上a的值就算是r中的值也無法使用
請問該如何修正呢?

看更多先前的討論...收起先前的討論...
你R本身是什麼東西?
a500197 iT邦新手 5 級 ‧ 2020-06-01 17:05:15 檢舉
LINQ後的結果
你可以給完整的程式碼?
a500197 iT邦新手 5 級 ‧ 2020-06-01 17:12:42 檢舉
這樣可以嗎?
我大概知道問題了 等我一下
a500197 iT邦新手 5 級 ‧ 2020-06-01 17:17:51 檢舉
好的感謝大大出手相助!
苦惱一段時間了
都想放棄直接用SWITCH CASE硬幹出來了...

2 個回答

1
小碼農米爾 Mir
iT邦研究生 1 級 ‧ 2020-06-03 05:49:27
最佳解答

研究後找到三個解法,提供給你。

//測試資料

 Id |  a  |  b
----|-----|-----
 1  | 123 | abc
 2  | 456 | def

1. 使用 SQL 語法

var column = "a";
var value = "12";
var test = await _db.Tests
    .FromSql($"SELECT * FROM Test WHERE {column} LIKE @value",
        new SqlParameter("value", $"%{value}%"))
    .Select(it => new
    {
        it.a,
        it.b
    })
    .ToListAsync();

//結果: [{"a":"123","b":"abc"}]
exec sp_executesql N'SELECT [it].[a], [it].[b]
FROM (
    SELECT * FROM Test WHERE a LIKE @value
) AS [it]',N'@value nvarchar(4)',@value=N'%12%'
go

2. 使用運算式樹產生 LIKE 語法

https://stackoverflow.com/questions/58320767/linq-where-ef-functions-like-why-direct-properties-work-and-reflection-does-no

使用文章中的 Search 函數

var column = "a";
var value = "12";
var test = await _db.Tests
    .Search(column, value)
    .Select(it => new
    {
        it.a,
        it.b
    })
    .ToListAsync();
    
//結果: [{"a":"123","b":"abc"}]
SELECT [item].[a], [item].[b]
FROM [Test] AS [item]
WHERE [item].[a] LIKE N'%12%'

3. 自己做出 it.a.Contains(value) 的運算式樹

既然是想產生 .Where(it => it.a.Contains(value)) 這段語法
我們就直接做出這段的運算式樹就好了。 (́◕◞౪◟◕‵)*

新增擴充方法

public static IQueryable<T> WhereContains<T>(this IQueryable<T> source, 
    string column, string value)
{
    //取得 T.column 屬性
    var property = typeof(T).GetProperty(column);
    
    //產生 it.a 的 it
    var itParameter = Expression.Parameter(typeof(T), "it");
    
    //產生 it.a
    Expression expressionProperty = Expression.Property(
        itParameter, property.Name);
    
    //產生 string.Contains 函數
    var containsMethod = typeof(string)
        .GetMethod("Contains", new[] { typeof(string) });

    //產生 it.a.Contains(value)
    var selector = Expression.Call(
        expressionProperty,
        containsMethod,
        Expression.Constant(value));
    
    //產生 .Where(it => it.a.Contains(value))
    return source.Where(Expression.Lambda<Func<T, bool>>(selector, itParameter));
}

呼叫 WhereContains 函數就能產生 .Where(it => it.a.Contains(value)) 的語法

var column = "a";
var value = "12";
var test = await _db.Tests
    .WhereContains(column, value)
    .Select(it => new
    {
        it.a,
        it.b
    })
    .ToListAsync();
    
//結果: [{"a":"123","b":"abc"}]
SELECT [it].[a], [it].[b]
FROM [Test] AS [it]
WHERE CHARINDEX(N'12', [it].[a]) > 0

結語

比較一下 EF 原生語法

var value = "12";
var test = await _db.Tests
    .Where(it => it.a.Contains(value))
    .Select(it => new
    {
        it.a,
        it.b
    })
    .ToListAsync();
exec sp_executesql N'SELECT [it].[a], [it].[b]
FROM [Test] AS [it]
WHERE (CHARINDEX(@__value_0, [it].[a]) > 0) OR (@__value_0 = N'''')',N'@__value_0 nvarchar(4000)',@__value_0=N'12'

可以發現原生語法都有使用 sp_executesql
而我們使用運算式樹產生的語法則沒有
所以需要考慮一下安全性

目前不知道如何在運算式樹呼叫 sp_executesql
感覺是一個很深的坑

小弟目前的研究還無法填滿
所以就先這樣摟

我使用的版本是 EF Core 2.2.6


2020/06/03 更新

找到解法了,參考這兩篇
https://stackoverflow.com/questions/32274259/using-func-property-selector-with-entity-framework-6
https://stackoverflow.com/questions/278684/how-do-i-create-an-expression-tree-to-represent-string-containsterm-in-c

我們在產生 it.a.Contains(value)
需要將 Expression.Constant(value) 內的 value 包裝成物件
EF 才會將其轉換成 SQL 參數

//將 value 包裝成物件
var closure = new { value = value };

//產生 { value = 12 }.value
var valueProperty = Expression.Property(
    Expression.Constant(closure),
    nameof(closure.value));

完整程式

public static IQueryable<T> WhereContains<T>(this IQueryable<T> source, 
    string column, string value)
{
    //取得 T.column 屬性
    var property = typeof(T).GetProperty(column);

    //產生 it.a 的 it
    var itParameter = Expression.Parameter(typeof(T), "it");
    
    //產生 it.a
    Expression expressionProperty = Expression.Property(
        itParameter, property.Name);
    
    //產生 string.Contains 函數
    var containsMethod = typeof(string)
        .GetMethod("Contains", new[] { typeof(string) });
        
    //將 value 包裝成物件
    var closure = new { value = value };
    
    //產生 { value = 12 }.value 
    var valueProperty = Expression.Property(
        Expression.Constant(closure),
        nameof(closure.value));

    //產生 it.a.Contains({ value = 12 }.value)
    var selector = Expression.Call(
        expressionProperty,
        containsMethod,
        valueProperty);
        
    //產生 .Where(it => it.a.Contains({ value = 12 }.value))
    return source.Where(Expression.Lambda<Func<T, bool>>(selector, itParameter));
}

結果就和原生的語法完全一樣了。 ╰( ̄▽ ̄)╭

exec sp_executesql N'SELECT [it].[a], [it].[b]
FROM [Test] AS [it]
WHERE (CHARINDEX(@__value_0, [it].[a]) > 0) OR (@__value_0 = N'''')',N'@__value_0 nvarchar(4000)',@__value_0=N'12'

參考文章

https://tyrrrz.me/blog/expression-trees
https://stackoverflow.com/questions/58320767/linq-where-ef-functions-like-why-direct-properties-work-and-reflection-does-no
https://stackoverflow.com/questions/30725363/how-to-call-a-method-with-instance-using-expression

看更多先前的回應...收起先前的回應...
a500197 iT邦新手 5 級 ‧ 2020-06-03 10:03:32 檢舉

非常感謝大大精闢完整的解答
其中甚至每條code都有相應註解真是簡顯易懂

/images/emoticon/emoticon41.gif

更新

我找到一個更簡單的方法
可以使用 EF.Property 取得欄位
原來 EF 有提供這種功能,不用自己硬寫
/images/emoticon/emoticon16.gif

var column = "a";
var value = "12";
var test = await _db.Tests
    .Where(it => 
        EF.Property<string>(it, column).Contains(value))
    .Select(it => new
    {
        it.a,
        it.b
    })
    .ToListAsync();
exec sp_executesql N'SELECT [it].[a], [it].[b]
FROM [Test] AS [it]
WHERE (CHARINDEX(@__value_0, [it].[a]) > 0) OR (@__value_0 = N'''')',N'@__value_0 nvarchar(4000)',@__value_0=N'12'
a500197 iT邦新手 5 級 ‧ 2020-06-04 09:33:35 檢舉

WOW~

a500197 iT邦新手 5 級 ‧ 2020-06-04 09:45:54 檢舉

請問EF有需要另外using什麼才能使用嗎?
發現我的vs沒有內建的修正
另外好奇一下您每次代碼下面的SQL語法是怎麼產生的呢?

我有用到這幾個套件
可能版本不一樣功能會有些差異

  • Microsoft.EntityFrameworkCore 2.2.6
  • Microsoft.EntityFrameworkCore.Relational 2.2.6
  • Microsoft.EntityFrameworkCore.SqlServer 2.2.6
  • Microsoft.Extensions.DependencyInjection 3.0.0

SQL 語法是用 SQL Server Profiler 攔截的

EF.Property 是 Microsoft.EntityFrameworkCore 內的函數

a500197 iT邦新手 5 級 ‧ 2020-06-04 13:47:39 檢舉

好的了解
感謝資訊提供!

a500197 iT邦新手 5 級 ‧ 2020-06-04 13:56:34 檢舉

剛試了一下
發現使用EF.Property去弄的話會報錯
跟樓下那個方法一樣的錯誤
判斷是LINQ不接受這種方式
目前還是使用您先前的方法

好喔,看來 EF.Property 不是每個版本都適用

3
qaz11226633
iT邦新手 5 級 ‧ 2020-06-01 17:23:26
public void test(string a = "id", string search = "4")
{
	R r1 = new R
	{
		id = "1234",
		projectname = "321",
		type = "123456",
		name = "456"
	};
    
	R r2 = new R
	{
		id = "456",
		projectname = "456789",
		type = "987",
		name = "888"
	};
    
    R[] r={r1,r2};
    
	if (search != "")
	{
		r = r.Where(x=>(x.GetType().GetProperty(a).GetValue(x).ToString().Contains(search))).ToArray();
	}
}
//你自訂select類別
public class R
{
	public string id { get; set; }
	public string projectname { get; set; }
	public string type { get; set; }
	public string name { get; set; }
}

這樣子就可以了
可以去讀下Type 類別相關資料

看更多先前的回應...收起先前的回應...
a500197 iT邦新手 5 級 ‧ 2020-06-01 17:28:25 檢舉

大大您好
預期目標是將傳入的a值作為x.後那個值的變動依據
例如a將傳入"id","projectname","type","name"
這種感覺

喔喔 這我需要想一下了

a500197 iT邦新手 5 級 ‧ 2020-06-01 17:36:20 檢舉

不好意思讓您誤會了

a500197這樣子就可以了

a500197 iT邦新手 5 級 ‧ 2020-06-02 09:27:48 檢舉

System.NotSupportedException: 'LINQ to Entities 無法辨識方法 'System.Object GetPropertyValue(System.Object, System.String)' 方法,而且這個方法無法轉譯成存放區運算式。'
出現了這個錯誤代碼
我研究一下哪邊出錯了
感謝大大幫助

a500197你試試自訂你的資料類別記得要{ get; set; }試試看

米歐 iT邦新手 4 級 ‧ 2020-06-02 22:31:14 檢舉

也可以試試看 GetFields()

我要發表回答

立即登入回答