ImplicitQueryString - 暗黙的変換を活用したC#用のクエリストリング変換ライブラリ

QueryStringかったるいですね、変換するのが。intに。boolに。それ以外に。そのままじゃどうしようもなくストレスフル。そこで、以下のように書けるライブラリを作りました。勿論NuGetでのインストールも可能です。あと、ライブラリといっても例によって.csファイル一個だけなので、導入は超お手軽。

以下はASP.NETの例ですが、NameValueCollectionを使っているものは全て対象になります。QueryStringだけじゃなくてRequest.Formなどにも使えますね。

// using Codeplex.Web;

int count;
string query;
bool? isOrdered; // support nullable

protected void Page_Load(object sender, EventArgs e)
{
    count = Request.QueryString.ParseValue("c");
    query = Request.QueryString.ParseValue("q");
    isOrdered = Request.QueryString.ParseValue("ord");
}

NameValueCollectionへの拡張メソッドとして実装しているので、using Codeplex.Webを忘れないように。ポイントは「型の明示が不要」というところです。クエリストリングを解析したい時って、通常はフィールドで既に変数自体は作っているのではないかと思います。なら、代入時に左から型を推論させてしまえばいいわけです、モダンなC#はわざわざ明示的に型なんて書かない、書きたくない。なのでこのライブラリ、ImplicitQueryStringではParseValueだけで全ての基本型(int, long, bool, string, DateTimeなど)へと型指定不要で代入可能となっています。代入先の型がNullableである場合は、キーが見つからなかったりパースに失敗した場合はnullにすることもサポートしてます。

また、よくあるクエリストリングはUrlEncodeされているのでデコードしたかったりするケースにも簡単に対応しています、UrlDecodeを渡すだけ!他にもEnumへの変換も出来ますし、キーが見つからなかったり変換不可能だったりした場合は指定したデフォルト値を返すParseValueOrDefault/ParseEnumOrDefaultも用意してあります。キーがあるかチェックするContainsKeyも。

enum Sex
{
    Unknown = 0, Male = 1, Female = 2
}

enum BloodType
{
    Unknown, A, B, AB, O
}

int age;
string name;
DateTime? requestTime;  // nullableやDateTimeもサポート
bool hasChild;
Sex sex;               // enumもいけます
BloodType bloodType;

protected void Page_Load(object sender, EventArgs e)
{
     // こんなQueryStringがあるとして
    // a=20&n=John%3dJohn+Ab&s=1&bt=AB

    // ageは左から推論してintを返します
    age = Request.QueryString.ParseValue("a"); // 20

    // UrlDecodeしたstringが欲しい時は第二引数にメソッドそのものを渡すだけ
    name = Request.QueryString.ParseValue("n", HttpUtility.UrlDecode); // John=John Ab

    // 代入先の型がnullableの場合は、もしキーが見つからなかったりパースに失敗したらnullにしてくれます
    requestTime = Request.QueryString.ParseValue("t", HttpUtility.UrlDecode); // null

    // キーが見つからなかったりパースに失敗したら指定した値を返してくれます
    hasChild = Request.QueryString.ParseValueOrDefault("cld", false); // false
    
    // Enumの変換は数字の場合でも文字列の場合でも、どちらでも変換可能です
    sex = Request.QueryString.ParseEnum<Sex>("s"); // Sex.Male
    bloodType = Request.QueryString.ParseEnumOrDefault<BloodType>("bt", BloodType.Unknown); // BloodType.AB

    // ContainsKeyはキーの有無をチェックします
    var hasFlag = qs.ContainsKey("flg"); // false
}

これで、あらゆるケースでサクッと変換することが可能なのではかと思います。ちなみに、ParseValue/ParseEnumでキーが見つからなかった場合はKeyNotFoundExceptionを返します。ParseValueで失敗したらFormatException、ParseEnumで失敗したらArgumentExceptionです。この二つが分かれているのは、int.Parseとかに渡しているだけなので、そちらの都合です。

仕組み

ImplicitQueryStringという名前がネタバレなのですが、単純に暗黙的変換をしているだけです。左から推論とか、ただのハッタリです。neue cc - dynamicとQueryString、或いは無限に不確定なオプション引数についてを書いたときに、わざわざdynamicを持ち出さなくても、暗黙的変換で十分じゃないの?そのほうが利便性も上じゃないの?と思ったのでやってみたら、やはりドンピシャでした。

暗黙的変換は、あまり使う機能じゃあないと思いますし、実際、乱用すべきでない機能であることには違いないのですが、たまに活用する分には中々刺激的で創造性に満ちています。大昔からあるけれど普段使わない機能だなんて、ロストテクノロジーっぽくて浪漫に溢れています。

さて、ただし活用するにはコード中に型を並べなければならないので、汎用的というわけではないのもそうなのですが、人力で書くのもシンドイところ。勿論人力でやる気はしないのでT4 Templateを使いました。

<#@ template language="C#" #>
<#@ output extension="cs" #>
<#@ assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#
    var methods = typeof(Convert).GetMethods(BindingFlags.Static | BindingFlags.Public);

    var converters = methods.Where(x => x.Name.StartsWith("To"))
        .Select(x => Regex.Replace(x.Name, "^To", ""))
        .Where(x => !x.StartsWith("Base64") && x != "String")
        .Distinct()
        .ToArray();
#>
using System;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace Codeplex.Web
{
    public static class NameValueCollectionExtensions
    {
        public static ConvertableString ParseValue(this NameValueCollection source, string key)
        {
            return ParseValue(source, key, null);
        }

        public static ConvertableString ParseValue(this NameValueCollection source, string key, Func<string, string> converter)
        {
            var values = source.GetValues(key);
            if (values == null) return new ConvertableString(null);

            var value = values[0];
            return new ConvertableString(converter == null ? value : converter(value));
        }
        
        // 中略
    }

    public struct ConvertableString
    {
        public readonly string Value;

        public ConvertableString(string value)
        {
            this.Value = value;
        }

<# foreach (var converter in converters) { #>
        public static implicit operator <#= converter #>(ConvertableString self)
        {
            if (self.Value == null) throw new KeyNotFoundException();
            return <#= converter #>.Parse(self.Value);
        }

        public static implicit operator <#= converter #>?(ConvertableString self)
        {
            <#= converter #> value;
            return (self.Value != null && <#= converter #>.TryParse(self.Value, out value))
                ? new Nullable<<#= converter #>>(value)
                : null;
        }

<# } #>
        public static implicit operator String(ConvertableString self)
        {
            return self.Value;
        }

        public override string ToString()
        {
            return Value;
        }
    }
}

以上のコードから、T4生成部分を取り出すと

public static implicit operator Boolean(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Boolean.Parse(self.Value);
}

public static implicit operator Boolean?(ConvertableString self)
{
    Boolean value;
    return (self.Value != null && Boolean.TryParse(self.Value, out value))
        ? new Nullable<Boolean>(value)
        : null;
}

public static implicit operator Char(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Char.Parse(self.Value);
}

public static implicit operator Char?(ConvertableString self)
{
    Char value;
    return (self.Value != null && Char.TryParse(self.Value, out value))
        ? new Nullable<Char>(value)
        : null;
}

// 以下SByte, Byte, Int16, Int32, Int64, ..., DateTimeと繰り返し

といったコードがジェネレートされます。清々しいまでのゴリ押しっぷり。一周回ってむしろエレガント。ConvertableStringのほうでKeyNotFoundExceptionを出すのがイケてないのですが、まあそこはShoganaiかなー、ということで。さて、それはそれとして、やっぱVisual Studioに組み込みで、こういった自動生成のテンプレートエンジンが用意されているというのは非常に嬉しいところです。

まとめ

特にASP.NETを使っている方々にどうぞ。ASP.NET MVCの人はしらにゃい。なのでいつもの私だったら.NET 4.0専用にするところが、今回は.NET 3.5でも大丈夫!Visual Studio 2008でも動作確認取りました!

それにしても実際ASP.NETを使いこなしてる人はクエリストリングの取得はどんな風にやっていたものなのでしょうかー。何かしらの手法がないとシンドすぎやしませんか?そう思って検索してみたんですが、どうにも見つからず。さすがに ParseValueAsInt() とかって拡張メソッドぐらい作ってやり繰りは私も大昔していたし、それでintに対応しておけばほぼほぼOKではありますが(拡張メソッドがなかった時代はさすがに想像したくない!)。そう考えると、ちょっとこれはやりすぎ感も若干。でも、一度できあがればずっと使えますしね。というわけでImplicitQueryString、是非是非使ってみてください。本当に楽になれると思います。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive