菜雞抓蟲: 使用 FromUri 的複雜型別在有傳遞 QueryString 的情況下會先建立再賦值
事發緣由
在 .net Framework 4.6.2 MVC 的 ApiController 中,某個查詢資料列表的方法除了提供查詢條件的參數以外,還有提供選擇性的分頁參數。也就是像這樣子:
[HttpGet]
public IEnumerable<Boo> GetBoos(
[FromUri] SearchBooParameter parameter,
[FromUri] PagingParameter paging = null)
{
// 呼叫 Service 查資料...
}
由於需要調整該功能的預設排序,改為由大到小,又不想背負更改大量共用的 PagingParameter
去影響到其他使用到的地方,決定在 Controller 這裡簡單用預測值加上判斷處理一下就好
相信著「若使用者沒有傳遞 paging 相關的參數,應該就會是給定的預設值 null
吧!」的我,用了 if (paging is null)
進行判斷:若是 null
的情況就將其中用來標示排序方向的成員 isDesc
設定為 true,開開心心交差。
[HttpGet]
public IEnumerable<Boo> GetBoos(
[FromUri] SearchBooParameter parameter,
[FromUri] PagingParameter paging = null)
{
if (paging is null)
{
paging = new PagingParameter();
paging.isDesc = true; // 預設由大到小
}
// 呼叫 Service 查資料...
}
但實際使用之後發現:即使只有傳入查詢條件參數、未傳遞 paging 時,資料仍然由小到大顯示,且 paging.isDesc
竟然是 false,並未被更改到。也就是說,即使未傳遞 paging,它也並不是 null!
實測之後發現:若在呼叫該 API 的時候,給定一個完全無關的參數,例如 ?a=1
,則 paging 還是會被建立一個實體出來,並無視 = null
這個預設值。因此就導致了非預期(=跟我想的不一樣啊!)的行為。
這邊直接先講結論:如果有傳遞 QueryString 的任何參數時,不管這些參數跟指定的類別有沒有關係,放在 [FromUri]
的複雜型別都會先建立出實體,再嘗試和 QueryString 的內容進行比對與設值。
設定在 [FromUri]
的複雜型別身上的預設值,像是 [FromUri] PagingParameter paging = null
只有完全沒給任何 QueryString 的時候才會吃到。(不過因為預設值只能是常數的關係,基本上就是指 defualt 的 null)
因此如果遇到要給定預設值的場合,還是得乖乖地針對型別中的成員做設定比較保險,例如 bool isDesc { get; set; } = true
。另外,因為完全沒給 QueryString 的時候還是會是 null,故該有的參數檢查仍然不能漏了。
測試案例
.net Framework 4.7.2 Web Api - FromUri
[RoutePrefix("api")]
public class ValuesController : ApiController
{
[HttpGet, Route("Test")]
public string Test([FromUri] SimplyParameter parameter = null)
{
return parameter?.IsSuccess.ToString() ?? "It's NULL!"; // False
}
public class SimplyParameter
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
}
}
/api/test
=> It’s NULL!
/api/test?a=2
=> False
若改為必須參數,而不是選擇性參數呢:
[HttpGet, Route("Test")]
public string Test([FromUri] SimplyParameter parameter)
{
return parameter?.IsSuccess.ToString() ?? "It's NULL!"; // False
}
/api/test
=> It’s NULL!
/api/test?a=2
=> False
看起來最大的差異點在於有沒有給 QueryString。
現在讓我們對 Model 給定預設值,並加入建構式並觀察執行順序:
[RoutePrefix("api")]
public class ValuesController : ApiController
{
[HttpGet, Route("Test")]
public string Test([FromUri] SimplyParameter parameter = null)
{
return parameter?.IsSuccess.ToString() ?? "It's NULL!"; // False
}
public class SimplyParameter
{
public SimplyParameter()
{
Console.WriteLine("I'm Creating!");
}
public bool IsSuccess { get; set; } = true;
public string Message { get; set; } = "It's me!";
}
}
/api/test?a=2
=> True
並且在 “It’s me!” 下中斷點,可以觀察到在進入 Test 前,的確建立了 SimplyParameter 並給予預設值
/api/test?IsSuccess=true
中斷點的執行順序為:
- 建立 SimplyParameter
- 執行
public bool IsSuccess = true;
給予初始值 - 執行
public string Message = "It's me!"
給予初始值 - 呼叫
SimplyParameter()
建構式,觸發"I'm Creating!"
- 呼叫
IsSuccess
的set
,將 QueryString 傳遞的值塞進去 - 對於 QueryString 未提供的
Message
,則未呼叫其set
,保持預設值 - 回到
string Test()
方法,SimplyParameter
已建立,無視parameter = null
看起來當有傳遞 QueryString 的時候,就會先建立需要的 Model 再逐一嘗試設值。
接著讓我們試試看,當 [FromUri]
的複雜型別不只一個的時候會怎麼運作呢:
[RoutePrefix("api")]
public class ValuesController : ApiController
{
[HttpGet, Route("Test")]
public string Test(
[FromUri] SimplyParameter parameter = null,
[FromUri] AnotherSimplyParameter parameterAnother = null)
{
return parameter?.IsSuccess.ToString() ?? "It's NULL!"; // False
}
public class SimplyParameter
{
public SimplyParameter()
{
Console.WriteLine("I'm Creating!");
}
public bool IsSuccess { get; set; } = true;
public string Message { get; set; } = "It's me!";
}
public class AnotherSimplyParameter
{
public AnotherSimplyParameter()
{
Console.WriteLine("I, Another me, are Creating!");
}
public int Idx { get; set; } = 999;
public string Name { get; set; } = "WOW";
}
}
/api/test?a=2
SimplyParameter 和 AnotherSimplyParameter 會依照 Test()
中傳入的順序,各自經歷一次上述的流程,故兩者都不會是 Null
/api/test?IsSuccess=true
即使傳入的參數只有其中一個 Model 具有符合的欄位,但由於上述的順序是「先建立,再比對」,故仍然會按上述流程分別建立兩者,仍然不會是 Null
/api/test?parameter.IsSuccess=true
即使已經具名指定了 SimplyParameter,但動作和 IsSuccess=true
仍然一樣
最後,我們加入一個簡單型別試試看,也就是改為如下:
public string Test(
[FromUri] SimplyParameter parameter = null,
[FromUri] AnotherSimplyParameter parameterAnother = null,
[FromUri] string hello = "world!")
/api/test
=> hello = “world!”;
/api/test?a=2
=> hello = “world!”;
簡單型別的預設值在運作上非常直覺,沒有什麼太大的問題。
[HttpGet, Route("Test")]
public string Test(
[FromUri] string hello,
[FromUri] SimplyParameter parameter = null,
[FromUri] AnotherSimplyParameter parameterAnother = null)
/api/test?hello=A
即使我們已經給了作為必要參數的簡單型別值,但由於同樣是從 Uri 來的,並不會在把值丟給簡單型別後就停止,兩個可選的複雜型別仍然會被建立
小結
- 在指定複雜型別為
[FromUri]
的場合,有以下注意事項- 有傳遞 QueryString 的參數時,放在
[FromUri]
的複雜型別會先建立出實體,再嘗試和 QueryString 的內容進行比對與設值。- 也因為會先建立實體,故選擇性參數的預設值,例如
parameter = null
或是parameter = default
並沒有效果,而是以該類型中各成員的預設值為主 - 因此如果遇到要針對各參數給定初始值的場合,請不要直接設定在
[FromUri]
的複雜型別身上再偷雞手動給值,而是乖乖地針對型別中的成員做設定
- 也因為會先建立實體,故選擇性參數的預設值,例如
parameter = default
當且僅當沒有傳遞任何 QueryString 的時候才有效(不過原本都沒給就會是 Default ,也就是 null,所以有沒有效我肉眼也分不太出來)
- 有傳遞 QueryString 的參數時,放在
- 然後關於一開始的問題,最後認命地請使用端傳參數解決了,耶嘿
同場加映
.net Core 3.1 Web Api - FromQuery
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
public string Get([FromQuery] SimplyParameter parameter = null)
{
return parameter?.IsSuccess.ToString() ?? "It's NULL!"; // False
}
}
public class SimplyParameter
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
}
/test
=> False
不愧是 Core,直接就建實體了,畢竟都說是 FromQuery
了嘛。既然都直接說會從 Query 來了,就沒在跟你五四三看有沒有 QueryString 的啦 XD
參考資料
其他文章
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)