前言
以前就有寫過了 Asp.net core 學習筆記 (操作 URL 和 Query), 但很亂, 這篇作為整理.
Uri 介紹
結構:
[Scheme]://[Host]:[Port][/Path][?Query][#Fragment]
對應:
[https:]//[www.stooges.com.my]:[443][/blog/air%20service][?title=my%20title][#my-id]
path 和 query 都需要 encode.
%20 就是空格 (space) 的 encode.
ASP.NET Core 有 2 個方法用來 encode 它們
var path = new PathString("/first name?name=derrick yam"); // must start with / var encodedPath = path.ToString(); // /first%20name%3Fname=derrick%20yam var queryString = $"?name={UrlEncoder.Default.Encode("derrick yam")}"; // ?name=derrick%20yam
類似 JavaScript 的 encodeURI 和 encodeURIComponent
Read URI (from request)
在 讀寫 Request / Response 有提到過如何獲取 request URI. 這里補充一些細節.
1. Fragment 是無法從 request URI 獲取的. 因為那個是 for client side 的.
2. HostString 對象
HostString hostString = HttpContext.Request.Host; string host = HttpContext.Request.Host.Host; // www.stooges.com.my int? port = HttpContext.Request.Host.Port; // 44300 string hostAndPort = HttpContext.Request.Host.ToString(); // www.stooges.com.my:44300
3. PathString 對象
PathString pathString = HttpContext.Request.Path; string? path = pathString.Value; // "/contact me/dada" string path1 = pathString.ToString(); // "/contact%20me/dada"
注意: 1 個有 decode 1 個沒有 decode
4. QueryString 對象
和 PathString 差不多接口
QueryString queryString = HttpContext.Request.QueryString; string query1 = queryString.ToString(); // "?page=%201&value=xx" string? query2 = queryString.Value; // "?page=%201&value=xx"
注意: 2 個都是沒有 decode 的哦,
5. Query 對象
QueryString 沒有太多的作用, 真的想方便的話用 Query
if (HttpContext.Request.Query.TryGetValue("page", out StringValues values)) { // ?page=1&page=%202 string value1 = values.ElementAt(0); // "1" string value2 = values.ElementAt(1); // " 2" var values3 = values.ToString(); // "1, 2" }
注意: 它是 decode 好的哦. 當有重復 key 的時候, value 會放到 values 里面.
也可以遍歷它.
IEnumerable<KeyValuePair<string, StringValues>> keyValuePairs = HttpContext.Request.Query; foreach (var kv in keyValuePairs) { string key = kv.Key; var value = kv.Value.ToString(); // "1, 2" }
注意: key 是不會重復的, 因為已經合並到 values 里的.
5.1 Query shortcut
也可以直接用 ["key"] 的方式拿來 compare 這樣會比較方便
var value1 = Request.Query["value"] == StringValues.Empty; // 當沒有 key 的時候 var value2 = Request.Query["value"] == ""; // value= 當有 key 但沒有值的時候 var value3 = Request.Query["value"] == "a"; // value=a 當有 key 有值的時候 var value4 = Request.Query["value"] == "a,b"; // value=a&value=b 當有 2 個 key 的時候 或者 value=a%2Cb 當有 1 個 key
6. GetDisplayUrl
它是 extension Microsoft.AspNetCore.Http.Extensions, 獲取當前完整路徑.
注意:
它不包含 #Fragment, Path 會 decode, Query 不會 decode. 和上面的邏輯是一致的.
如果想獲取沒有 decode 的 URL, 可以調用 GetEncodedUrl()
Read URI (from string)
var uri = new Uri("https://www.stooges.com.my/about%20us/detail?page=1#my-id"); string scheme = uri.Scheme; // https string host = uri.Host; // www.stooges.com.my int port = uri.Port; // 443 string path = uri.LocalPath; // /about us/detail (decoded) string absolutePath = uri.AbsolutePath; // "/about%20us/detail" string query = uri.Query; // ?page=1 string fragment = uri.Fragment; // #my-id string pathAndQuery = uri.PathAndQuery; // "/about%20us/detail?page=1"
幾個點注意:
1. Frament 可以拿到
2. Path, Query, Fragment 分別 starts with "/", "?", "#"
3. Query 是沒有 decode 的
4. LocalPath 有 decode, AbsolutePath 沒有 decode, PathAndQuery 沒有 decode
5. 除了 port 全部返回都是 string 而不是 HostString, PathString, QueryString 這類的.
比起 read URI from request, new Uri 簡單多了. 它的 Query 最好交給 QueryHelper 處理哦.
另外, new Uri 默認必須是絕對路徑
var uri = new Uri("/test"); // 報錯
如果想放相對路徑要特別聲明 UriKind.Relative
var uri = new Uri("/test", UriKind.Relative); var uri = new Uri("test", UriKind.Relative);
注: relative URI 不支持獲取 query 哦.uri.Query 會直接報錯.
C# 沒有很好的方法可以從 relative URI 拿到 query string. 參考: Stack Overflow – How to get the parameter from a relative URL string in C#?
QueryHelpers
Request.Query 很方便, 但是 Uri 沒有這些. 所以需要 QueryHelpers 幫忙
var uri = new Uri("https://www.stooges.com.my?page=1&page=%202"); string queryString = uri.Query; // ?page=1&page=%202 Dictionary<string, StringValues> query = QueryHelpers.ParseQuery(queryString); var value1 = query["page"].ElementAt(0); // "1" var value2 = query["page"].ElementAt(1); // " 2" decoded var value3 = query["page"].ToString(); // "1, 2" foreach (KeyValuePair<string, StringValues> kv in query) { string key = kv.Key; // page StringValues values = kv.Value; }
用起來很簡單, 把 Uri.Query 傳給 QueryHelpers 就可以了, 基本上就有了 Request.Query 的所有能力了.
返回字典, 字典繼承了 IEnumerable<KeyValuePair<string, StringValues>> 所以也可以遍歷拿到 KeyValuePair.
注: ParseQuery 的參數一定要是 query string, 可以 starts with ? 或者直接 key=value, 但不可以 starts with path 哦, 比如 "/login?name=derrick" 這樣是 parse 失敗的.
QueryBuilder
介紹完 read, 現在介紹 build. 先從簡單的 build query 開始.
var queryBuilder = new QueryBuilder(); queryBuilder.Add("page", "1"); queryBuilder.Add("page", " 2"); queryBuilder.Add("value", "abc"); QueryString queryString = queryBuilder.ToQueryString(); var queryString1 = queryString.Value; // "?page=1&page=%202&value=abc" var queryString2 = queryString.ToString(); // "?page=1&page=%202&value=abc"
添加, 最后 ToQueryString 就可以了,
1. value 不需要自己 encode
2. 不管是 queryString.Value 還是 .ToString() 都是 encoded 的 (QueryString 都是 encoded 的上面介紹 read 的時候也有提到)
QueryBuilder 初始化還支持 KeyValuePairs 哦
var keyValuePairs = new Dictionary<string, StringValues> { ["page"] = new StringValues(new[] { "1", " 2" }), ["value"] = "abc" }; var queryBuilder = new QueryBuilder(keyValuePairs);
這樣和上面的例子是等價的效果.
注意: QueryBuilder 只能 add, 不能 set 也不能 remove.
QueryHelpers + QueryBuilder
把 read 和 write 加起來就變成了 modify.
var queryParams = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); queryParams["key1"] = "value11"; // edit value or add key value queryParams.Add("key3", "key3"); // add key // query.Add("key2", "key22"); // Error: can't add when key existed queryParams["key2"] = new StringValues(queryParams["key2"].Append("value22").ToArray()); // add more value to existing key queryParams.Remove("key2"); // all values under this key will be removed var queryBuilder = new QueryBuilder(queryParams); queryBuilder.Add("salary", "500"); // add extra key queryBuilder.Add("salary", "600"); // add more value to existing key (it will not get error like above) var newQueryString = queryBuilder.ToQueryString().Value; // ?key1=value11&key3=key3&salary=500&salary=600
1. queryParams.Add 會報錯, 當 key 已存在
2. queryBuilder.Add 不會報錯, 當 key 已存在
它沒有批量 remove multiple key 的方法, 必須先 filter 出來然后一個一個 remove
var keys = queryParams.Keys.Where(k => k != "page").ToList(); keys.ForEach(key => queryParams.Remove(key));
UriBuilder
var uriBuilder = new UriBuilder(); uriBuilder.Scheme = "https"; uriBuilder.Host = "www.stooges.com.my"; uriBuilder.Port = 443; // 通常是不需要放的, https 自動是 443, http 自動是 80 uriBuilder.Path = "/blog/air service"; // better starts with /, make it consistency uriBuilder.Query = "?page=1"; // better starts with ?, make it consistency uriBuilder.Fragment = "#my-id"; // better starts with #, make it consistency var url = uriBuilder.ToString(); // "https://www.stooges.com.my:443/blog/air%20service?page=1#my-id"
1. Path 不需要 encode
2. Query 需要 encode, 最好是通過 QueryBuilder 制作
3. Path, Query, Fragment 最好 starts with "/", "?", "#" 因為 Read 的時候是有的, 讓它們一致性.
總結
1. 通過 Request 獲取 information 比較 Uri 方便, 尤其是 Query 的部分, 因為它有封裝.
2. 可以借助 QueryHelpers 來讀取 Query, Request.Query 內部也是靠它的.
3. Path 都是自動 encode, decode 的.
4. Query string 需要 Builder, Helpers 來做幫助 encode, decode.