ASP.NET MVC - input要素のplaceholder属性に使うHtmlHelperの拡張メソッドを作ってみた

気持ち今さら感はありますが、input要素のplaceholder属性にテキストを出力するためのHtmlHelperの拡張メソッドを作ってみました。正直なところやってみて作りました!というほどではなかったんですが、まあ試してみたかったんです、ということで。

モデルのプロパティに対する表示名は、DisplayAttributeのNameプロパティとDisplayNameForメソッドを使えばいい感じに出力できます。そんなノリでできないかなと思って調べてみたところ、DisplayAttributeのPromtプロパティがそれっぽいかな?使っていいかな?と。MVCのソースも調べてみるとこのプロパティの値はModelMetadataのWatermarkプロパティから取得できそうかなと。

ということで作ってみた拡張メソッドがこちらです。

public static class PlaceholderExtesions {
    // placeholder属性用の文字列を取得
    public static MvcHtmlString PlaceholderFor<TModel, TValue>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TValue>> expression) {

        // モデルのメタデータを取得
        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

        // メタデータのウォーターマークを使う
        // html属性として使うと想定してエンコード
        return new MvcHtmlString(HttpUtility.HtmlAttributeEncode(metadata.Watermark));
    }
}

それでは使ってみます。まずはモデルを用意します。

// モデル
public class TestFormModel {
    // Promptにplaceholder属性に出力したい文字列を指定
    [Display(Name = "メールアドレス", Prompt = "メールアドレスを入力してください")]
    public string Mail { get; set; }
}

作ったHtmlHelperの拡張メソッドはこんな感じで使います。長いので適当に改行しています。

<input type="text"
    name="@Html.NameFor(model => model.Mail)"
    value="@Html.ValueFor(model => model.Mail)"
    placeholder="@Html.PlaceholderFor(model => model.Mail)" />

するとこんなhtmlが出力されると。

<input type="text" name="Mail" value="" placeholder="メールアドレスを入力してください" />

いい感じ。

LINQ - 空のシーケンスでMin/Max/Averageを使ったときのメモ

LINQで空のシーケンスに対してMin/Max/Averageを使うとInvalidOperationExceptionが発生する場合があります。言われるとそっかと思うんですが、SQLの感覚もあってかうっかりやっちゃうかなあと思ったのでちょっとメモしておきます。

たとえば、intの空のシーケンスに対してMinメソッドを使ってみます。この場合はInvalidOperationExceptionが発生します。int?の空のシーケンスだとnullが返ってきます。

// intの空のシーケンスの場合はInvalidOperationException
try {
    Enumerable.Empty<int>().Min();
} catch (InvalidOperationException exception) {
    Console.WriteLine($"{exception.Message}");
    //シーケンスに要素が含まれていません
}

// int?の空のシーケンスの場合はnull
var min = Enumerable.Empty<int?>().Min();
if (min == null) {
    Console.WriteLine($"{nameof(min)}はnull");
    //minはnull
}

selector関数を使ったオーバーロードも確認してみます。selector関数の戻り値がnull許容型かどうかでnullが返ってくる場合と例外が発生する場合があります。

// selector関数の戻り値が非nullの場合はInvalidOperationException
try {
    Enumerable.Empty<Tuple<DateTime>>().Min(tuple => tuple.Item1);
} catch (InvalidOperationException exception) {
    Console.WriteLine($"{exception.Message}");
    //シーケンスに要素が含まれていません
}

// selector関数の戻り値がnull許容型の場合はnull
var min = Enumerable.Empty<Tuple<DateTime?>>().Min(tuple => tuple.Item1);
if (min == null) {
    Console.WriteLine($"{nameof(min)}はnull");
    //minはnull
}

null許容型ではないシーケンスにMin/Max/Averageするときはシーケンスが空かどうかチェックしましょうってことですね。はい、気をつけます。

SqlConnectionの接続タイムアウトを確認してみる

SQL Serverで単にタイムアウトと言っても接続タイムアウトとクエリタイムアウト(コマンドタイムアウト)の2つがあります。発生するタイミングが違いますし、それぞれ理解しておきたいなと思います。

まずはちょっと簡単に整理します。

接続タイムアウト

名前の通りなんですがSQL Serverへ接続を試みるときのタイムアウト。SqlConnection.Openのときに発生する場合があります。

タイムアウトまでの時間はSqlConnectionStringBuilder.ConnectTimeoutプロパティで設定します。SqlConnection.ConnectionTimeoutプロパティからは参照だけ。

クエリタイムアウト(コマンドタイムアウト

こちらも名前のとおりですがSqlCommand.ExecuteNonQueryやSqlCommand.ExecuteReaderなどのタイムアウト。去年の今ごろSqlCommandでタイムアウトのときに発生するSqlExceptionを拾って確認してみましたが、今思うとクエリタイムアウトのことですね。

ichiroku11.hatenablog.jp

タイムアウトまでの時間はSqlCommand.CommandTimeoutプロパティで設定・参照できます。

接続タイムアウトを確認してみる

てことで接続タイムアウトを確認してみようと思います。接続に失敗する原因は色々あると思いますが試しにSQL Serverを停止して次のコードを実行してみるとSqlExceptionが発生します。

var connectionString = new SqlConnectionStringBuilder {
    DataSource = "lpc:.",    // 共有メモリプロトコル
    InitialCatalog = "Test",
    IntegratedSecurity = true,
    ConnectTimeout = 1,    // 試しに1秒でタイムアウトするように
}.ToString();

var connection = new SqlConnection(connectionString);
// タイムアウトは1秒になっている
Console.WriteLine($"{nameof(connection.ConnectionTimeout)}: {connection.ConnectionTimeout}");
//ConnectionTimeout: 1

try {
    // コネクションを開こうとして1秒ほどでタイムアウト(SqlExceptionがスロー)
    connection.Open();
} catch (SqlException exception) {
    Console.WriteLine($"{nameof(SqlException)}:");
    Console.WriteLine($"{nameof(exception.Number)}: {exception.Number}");
    Console.WriteLine($"{nameof(exception.Message)}: {exception.Message}");
    //SqlException:
    //Number: 2
    //Message: SQL Server への接続を確立しているときにネットワーク関連またはインスタンス固有のエラーが発生しました。サーバーが見つからないかアクセスできません。インスタンス名が正しいこと、および SQL Server がリモート接続を許可するように構成されていることを確認してください。 (provider: Shared Memory Provider, error: 40 - SQL Server への接続を開けませんでした)
} finally {
    connection.Close();
}

コネクションプールの最大接続数を超えた場合

実はこれこの前初めて経験しました。コネクションプールの最大接続数を超えてコネクションを開こうとするとInvalidOperationExceptionが発生します。

試しに最大接続数を2にした場合、コネクションを2つ開いた状態から3つ目のコネクションを開こうとして例外が発生します。

var connectionString = new SqlConnectionStringBuilder {
    DataSource = "lpc:.",
    InitialCatalog = "Test",
    IntegratedSecurity = true,
    MaxPoolSize = 2,   // 最大接続数
    ConnectTimeout = 1,    // 1秒でタイムアウト
}.ToString();

var connection1 = new SqlConnection(connectionString);
var connection2 = new SqlConnection(connectionString);
var connection3 = new SqlConnection(connectionString);

try {
    // コネクション2つまでは開く
    connection1.Open();
    Console.WriteLine($"{nameof(connection1)}: opened");
    //connection1: opened

    connection2.Open();
    Console.WriteLine($"{nameof(connection2)}: opened");
    //connection2: opened

    // 3つめのコネクションを開こうとして1秒ほどで例外
    connection3.Open();
} catch(InvalidOperationException exception) {
    Console.WriteLine($"{nameof(InvalidOperationException)}:");
    Console.WriteLine($"{nameof(exception.Message)}: {exception.Message}");
    //InvalidOperationException:
    //Message: タイムアウトに達しました。プールから接続を取得する前にタイムアウト期間が過ぎました。プールされた接続がすべて使用中で、プール サイズの制限値に達した可能性があります。
} finally {
    connection1.Close();
    connection2.Close();
    connection3.Close();
}

参考

SQL Server - 主キーの一覧を取得するクエリ

前回は外部キーの一覧を取得するクエリを書いてみました。

ichiroku11.hatenablog.jp

今回は外部キーのときと同じようにテーブル名やカラム名を含めて主キーの一覧を取得するクエリを書いてみました。

select
    i.name as [主キー名],
    ic.index_column_id as [主キーカラムID],
    t.name as [テーブル名],
    c.name as [カラム名]
from sys.indexes as i
    inner join sys.index_columns as ic
        on i.object_id = ic.object_id and i.index_id = ic.index_id
    -- 主キーがあるテーブルとカラムをjoin
    inner join sys.tables as t
        on t.object_id = i.object_id
    inner join sys.columns as c
        on ic.object_id = c.object_id and ic.column_id = c.column_id
where i.is_primary_key = 1
order by [主キー名], [主キーカラムID];

主キーを作成すると対応するインデックス(クラスター化か非クラスター化)が自動的に作成されて、主キーを削除すると対応するインデックスも削除されるとのことなのでsys.indexesカタログビューを使っています。(主キー制約の一覧はsys.key_constraintsから取得できるようなのでもしかするとそっちを使ったほうがいいのかも?違いはあまりないかも?)

主キーカラムIDは単体の主キーでは常に1で、複合主キーのときに1、2・・・nという値になります。

参考

SQL Server - 外部キーの一覧を取得するクエリ

テーブル名やカラム名を含めて外部キーの一覧を取得するクエリです。探したら似たようなクエリが見つかる気もしますが練習もかねて書いてみました。また使うことがあるかなと。

select
    fk.name as [外部キー名],
    fkc.constraint_column_id as [外部キーカラムID],
    pt.name as [参照元テーブル名],
    pc.name as [参照元カラム名],
    rt.name as [参照先テーブル名],
    rc.name as [参照先カラム名]
from sys.foreign_keys as fk
    inner join sys.foreign_key_columns as fkc
        on fk.object_id = fkc.constraint_object_id
    -- 参照元のテーブルとカラムをjoin
    inner join sys.tables as pt
        on fkc.parent_object_id = pt.object_id
    inner join sys.columns as pc
        on fkc.parent_object_id = pc.object_id and
            fkc.parent_column_id = pc.column_id
    -- 参照先のテーブルとカラムをjoin
    inner join sys.tables as rt
        on fkc.referenced_object_id = rt.object_id
    inner join sys.columns as rc
        on fkc.referenced_object_id = rc.object_id and
            fkc.referenced_column_id = rc.column_id
order by [外部キー名], [外部キーカラムID];

適当なサンプルテーブルを作ってクエリを実行した結果はこんな感じです。

-- Table2がTable1を参照している
create table Table1(
    Id int not null,
    constraint PK_Table1 primary key(Id)
);
create table Table2(
    Id int not null,
    Table1Id int not null,
    constraint FK_Table2_Table1 foreign key(Table1Id) references Table1(Id));

-- 結果
/*
外部キー名        外部キーカラムID 参照元テーブル名  参照元カラム名 参照先テーブル名 参照先カラム名
---------------- -------------- --------------- ------------ -------------- ------------
FK_Table2_Table1 1              Table2          Table1Id     Table1         Id
*/

複合外部キーの場合。

-- Table4がTable3を複合外部キーで参照している
create table Table3(
    Id1 int not null,
    Id2 int not null,
    constraint PK_Table3 primary key(Id1, Id2)
);
create table Table4(
    Id int not null,
    Table3Id1 int not null,
    Table3Id2 int not null,
    constraint FK_Table4_Table3 foreign key(Table3Id1, Table3Id2) references Table3(Id1, Id2));

-- 結果
/*
外部キー名        外部キーカラムID 参照元テーブル名  参照元カラム名 参照先テーブル名 参照先カラム名
---------------- -------------- --------------- ------------ -------------- ------------
FK_Table4_Table3 1              Table4          Table3Id1    Table3         Id1
FK_Table4_Table3 2              Table4          Table3Id2    Table3         Id2
*/

参考