前陣子談過用 HttpUtility.ParseQueryString 解析、修改及還原參數的簡便做法,一不做二不休,再來聊聊如果拿到一個 URL, HTTP 要改 HTTPS、主機名稱要換、Port、路徑要改,是不是也有不走字串比對置換的優雅做法?

爬文查到好用的 .NET 內建物件 - UriBuilder(.NET 2.0 時代就有了,我到現在才學會),Scheme(http、https)、Port、Host、Path 均可任意抽換調整,再產生新的 URL,比起自己寫 Regex 比對置省事可靠不少。UriBuilder 已滿足大部分所需,只少了將 QueryString 轉成 NameValueColletion 方便抽換修改參數,查了文件 UriBuilder 並未宣告 seal 禁止繼承,因此繼承 UriBuilder 再擴充參數處理功能是不錯的選擇。

我寫了一顆 FlexUrlEditor 繼承 UriBuilder,建構式傳入既有 URL,融和上回將 QueryString 轉為 NameValueCollection 增刪修改再轉回字串的技巧,參數可透過 FlexUrlEditor["參數名"] 存取,為 UriBuilder 加上編修 QueryString 參數功能。此外,UriBuilder 產出的 URL 字串有個小缺點 - 它包含 :80、:443 等習慣上會省略的 Port,所以我在輸出時加了點工,使結果更符合平日慣用格式。程式碼如下:

using System;
using System.Collections.Specialized;
using System.Text;
using System.Web;

public class FlexUrlEditor : UriBuilder
{
    private readonly NameValueCollection collection;

    public FlexUrlEditor(string url) : base(url)
    {
        collection = 
            HttpUtility.ParseQueryString(new Uri(url).Query, Encoding.UTF8);
    }

    public string this[string key]
    {
        get { return collection[key]; }
        set
        {
            if (value == null && collection[key] != null)
            {
                collection.Remove(key);
            }
            else
            {
                collection[key] = value;
            }
        }
    }

    public string GenUrl()
    {
        //REF: https://goo.gl/gHmGUz
        base.Query = Uri.EscapeUriString(
            HttpUtility.UrlDecode(collection.ToString()));
        //remove 80/443 port for http/https
        if (base.Uri.IsDefaultPort) base.Port = -1;
        return base.ToString();
    }
}

寫幾行程式實測,試了修改、刪除與增加 QueryString 參數,https 改 http,Port 變更等(Host 與 Path 也都可置換忘了示範),均得到預期的結果。

using System;

namespace ConsoleApp1
{
    class Program
    {

        static void Main(string[] args)
        {
            //Add parameter
            var url = "http://blog.darkthread.net/";
            var builder = new FlexUrlEditor(url);
            builder["a"] = "1";
            Console.WriteLine(builder.GenUrl());

            //modify and remove parameter
            url = "http://blog.darkthread.net/?a=1&b=2&c=3";
            builder = new FlexUrlEditor(url);
            builder["b"] = null; //set null to remove
            builder["c"] = "中文"; //will be encoded
            Console.WriteLine(builder.GenUrl());

            //single command (C# 6.0+)
            var newUrl = new FlexUrlEditor(
                "https://blog.darkthread.net/?a=1&b=2&c=3")
            {
                ["b"] = null,
                ["c"] = "中文"
            };
            Console.WriteLine(newUrl.GenUrl());

            //read encoded query parameter
            builder = new FlexUrlEditor(
                "http://blog.darkthread.net/?a=1&b=%E4%B8%AD%E6%96%87");
            Console.WriteLine($"a={builder["a"]}, b={builder["b"]}");

            //change scheme & port
            builder = new FlexUrlEditor("https://blog.darkthread.net");
            builder.Scheme = "http";
            builder.Port = 8888;
            Console.WriteLine(builder.GenUrl());

            Console.ReadLine();
        }
    }
}

執行結果如下:

工具箱再添順手工具一支~


Comments

# by Ammon

用 IsDefaultPort 判斷是否為預設Port,將Port設定為-1,ToString時不會額外加上Port if (Uri.IsDefaultPort) { Port = -1; }

# by Jeffrey

to Ammon, 這招漂亮! 感謝分享,已納入程式碼。

Post a comment


47 + 22 =