2013年3月22日金曜日

ASP.NET MVC のエラーハンドリング(未完)

ASP.NET MVC でのエラーハンドリングを見直す機会があったのでメモします。

これまで

今までエラーハンドリングは customErrors の設定と、Application_Error イベントで行なっていました。

<customErrors mode="RemoteOnly" defaultRedirect="~/Error">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
protected void Application_Error()
{
 var exception = Server.GetLastError();
 // 例外をロギングしたり...
}

こんな感じでエラーページの表示と適当に例外を起こすアクションを用意しました。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult NotFound()
    {
        return View();
    }

    public ActionResult Raise(int? httpCode)
    {
        if (!httpCode.HasValue)
            throw new ApplicationException("エラーだよ。");

        throw new HttpException(
            httpCode.Value, string.Format("ステータスコード {0} のエラーだよ。", httpCode));
    }
}

これで例外が起きると用意したエラーページが表示され、Application_Error で必要な処理(ロギングとか)を実行できます。

なにが問題?

この方法でもひと通りのことはできているんですが、よく見ると気になることがあります。

例外を起こしてエラーページを表示させてみます。このときの HTTP はこんな風になっています。

エラーページのステータスコードが "200"

エラーページなのにステータスコードが 200 になっています。
できれば 500 やその他適切なステータスコードを返したいところです。

リダイレクトによるエラーページのナビゲート

304 で customErrors で指定したエラーページの URL にリダイレクトしています。
これもできれば、リダイレクトせずにエラーページを表示できるほうが理想的です。

というわけで、"適切なステータスコード""リダイレクトせずに" エラーページを表示する方法を探しました。

エラーページのステータスコード

エラーページがそれぞれ適切なステータスコードで返るようにするのは簡単です。
Response.StatusCode を設定してあげれば OK です。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        return View();
    }

    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult Raise ...
}

これだけでこんな感じで設定したステータスコードでレスポンスが返ります。

リダイレクトせずにエラーページを表示する

次はリダイレクトせずにエラーページを表示する方法です。

customErrors の redirectMode="ResponseRewrite" ... ?

customerErrors には redirectMode というプロパティがあり、 リダイレクトしてエラーページをナビゲートする "ResponseRedirect" かレスポンスをエラーページの内容で上書きして表示する "ResponseRewrite" かを設定できます。

<customErrors mode="On" defaultRedirect="~/Error" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
残念ながら ASP.NET MVC ではうまく動かない

まさにコレ!という内容のプロパティですが、ASP.NET MVC では思ったように動作しません

ResponseRewrite ではレスポンスの内容を指定されたページの内容で上書きしてくれるのですが、 この際 ASP.NET は指定されたパスで物理ファイルを探しにいってしまいます。

なので、WebForm であればこれはうまく動作する(ハズ)のですが、ASP.NET MVC では動作しません。ざんねん...

物理ファイルで

物理ファイル探しにいくなら、物理ファイルで指定したら動くわけで... 指定しちゃってみます。

適当にエラーページを aspx で用意して、customErrors で指定します。
このとき aspx にはステータスコードを指定するコードを入れておくと任意のステータスコードでレスポンスを返せます。

protected override void OnLoad(EventArgs e)
{
    Response.StatusCode = (int)HttpStatusCode.InternalServerError;   
    base.OnLoad(e);
}
<customErrors mode="On" defaultRedirect="~/Views/Error/Index.aspx" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Views/Error/NotFound.aspx"/>
</customErrors>

動きますね。

これでいいのか

「レイアウト(_Layout.cshtml)とか使えないじゃん、コレ」とか「エラーページも Razor で書こうよ。」とか...

IIS7とかであれば httpErrors を使ってできたりするようです。

また、カスタムの HandleError を作ることでもできます。が、HandleError はあくまで ActionFilter なので、アクションメソッドの外側で起きるエラーには手出しできません。

一番いいのは ASP.NET MVC が redirectMode="ResponseRewrite" のときに物理ファイルをアテにするのではなく、パスの実行結果を上書きしてくれるようになることなんですが...

ここで時間切れ

いろいろ調べてみたんですが、なかなかコレ!っていう解決策が見つからない&思いつきませんでした。
もしいい方法をご存知の方がいらしたら教えて下さい。

2013年1月22日火曜日

ASP.NET MVC 4 の StyleBundle で注意すること

ASP.NET MVC 4 の StyleBundle を利用する際に気をつけたいことがあったのでメモします。

こんな感じでディレクトリとファイルを構成し、スタイルシートを用意した場合を見ていきます。

ファイル

├─Content
   ├─images
   │  └─picture.jpg
   └─site.css

スタイルシート(site.css)

.picture {
  background-image: url("images/picture.jpg");
}

この構成で次のようなバンドルを設定すると困ったことになります。

bundles.Add(new StyleBundle("~/Content/Site/css").Include(
    "~/Content/site.css"));

このバンドルで site.css が出力されるパスは "~/Content/Site/css" です。
背景に指定されている画像は相対パスで参照しているので、"/Content/Site/images/picture.jpg" というパスになってしまい、画像が参照できません。

このようにバンドル時のパスが実際のファイル構成と異なると、スタイルシートから相対パスで参照している画像などが読めなくなってしまうので注意が必要です。

実際のファイル構成と同じパスでバンドルしてあげる必要があります。

bundles.Add(new StyleBundle("~/Content/css").Include(
    "~/Content/site.css"));

また、違うパスにあるスタイルシートをまとめてバンドルするのも避けたほうがいいですね。

2013年1月21日月曜日

ASP.NET MVC 4 の ScriptBundle はデバッグ時に .min.js を出力しない

ASP.NET MVC 4 の ScriptBundle はファイル名の末尾で利用するファイルを選択します。

例えば以下の様な3つのファイルを用意した場合、

  • sample.js
  • sample.debug.js
  • sample.min.js

バンドルの設定を以下のようにした場合を見ていきます。

bundles.Add(new ScriptBundle("~/bundles/sample").Include("~/Scripts/sample*"));

デバッグ時

デバッグ時(web.config の compilation要素の debug属性が true のとき)は、sample.debug.js が選択されます。

<script src="/Scripts/sample.debug.js"></script>

デバッグ時でないとき

デバッグ環境でないとき(web.config の compilation要素の debug属性が false のとき)は、sample.min.js が選択されます。

また、sample.min.js の内容は minify されて出力されます。今回は一つのファイルですが、複数のファイルを一つのバンドルに設定した場合は、それらのファイルを全てくっつけて一つのファイルとしてレスポンスしてくれます。

<script src="/bundles/sample?v=Rk-zyJQ66YShCJwocw4z0jCjm_jhceIZ5m55SxsnveY1"></script>

sample.js のみの場合

sample.js のみの場合を見てみます。

デバッグ時

普通に sample.js が選択されます。

<script src="/Scripts/sample.js"></script>

デバッグ時でないとき

もちろん sample.js が選択されますが、スクリプトの内容は minify されて出力されます。

<script src="/bundles/sample?v=FleCpml6dOgPyNvg3fS5cE-_9YPt2osVzl-MIlVAIPU1"></script>

sample.min.js のみの場合

sample.min.js のみだったらどうなるでしょうか。

デバッグ時

この場合注意が必要です。何も読み込まれません。 Scriptタグもレンダリングされません。

デバッグ時でないとき

sample.min.js が選択され、minify されて出力されます。

<script src="/bundles/sample?v=Rk-zyJQ66YShCJwocw4z0jCjm_jhceIZ5m55SxsnveY1"></script>

sample.debug.js のみの場合

あまりないと思いますが、sample.debug.js のみではどうなるでしょうか。

デバッグ時

sample.debug.js が選択されます。

<script src="/Scripts/sample.debug.js"></script>

デバッグ時でないとき

スクリプトタグは出力されますが、中身は空です。

<script src="/bundles/sample?v="></script>

注意点

というわけで、xxxxx.min.js のみだとデバッグ時に利用できなくなってしまうので注意が必要です。
xxxxx.js を入手して一緒に入れておくか、xxxxx.debug.js が用意されていれば合わせて入れておくとよさそうです。

この記事を書く前は、xxxxx.min.js はデバッグ時でないときは minify されずにそのまま出力されると思っていましたが、minify されるようです。そのまま出してくれてもいいような気がするんですが...

2013年1月16日水曜日

[NHibernate]enumを文字列でマッピングする

NHibernate で enum のプロパティ/列をマッピングするときのメモです。

普通にマッピングすると

以下の様な enum のプロパティをマッピングすると、"Gender" は 1, "Female" は 2 という感じで enum の中身の整数(int)でマッピングされます。

モデル

public enum Gender
{
    Male = 1,
    Female = 2
}
public class Student
{
    public virtual int Id { get; set; }

    public virtual string Name { get; set; }

    public virtual Gender Gender { get; set; }
}

テーブル

テーブルのほうはこんな感じですね。

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] INT            NOT NULL,
    CHECK ([Gender] = 1 OR [Gender] = 2)
);

マッピング

マッピングはこうです。

<class name="Student" table="Students">

    <id name="Id">
        <generator class="identity"></generator>
    </id>

    <property name="Name"></property>
    <property name="Gender"></property>

</class>

データ

Id Name Gender
1 スタン 1
2 カイル 1
3 カートマン 1
4 ケニー 1
5 バターズ 1
6 ウェンディ 2

文字列でマッピングしたいときは

でも整数ではなく文字列でテーブルに持ちたいときもあります。データをこんな風にしたいときですね。

Id Name Gender
1 スタン Male
2 カイル Male
3 カートマン Male
4 ケニー Male
5 バターズ Male
6 ウェンディ Female

そんなときは NHibernate の IUserType インターフェイスを実装してマッピングに利用します。 例えばこんな感じの基底クラスを用意しておいて、

public class EnumStringType<TEnum> : IUserType
{
    public bool Equals(object x, object y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;
        return x.Equals(y);
    }

    public int GetHashCode(object x)
    {
        if (x == null) return 0;
        return x.GetHashCode();
    }

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        var value = NHibernateUtil.String.NullSafeGet(rs, names);

        if (value == null)
            return null;

        return (TEnum) Enum.Parse(typeof (TEnum), (string) value);
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        string stringValue = null;

        if (value != null)
            stringValue = ((TEnum) value).ToString();

        NHibernateUtil.String.NullSafeSet(cmd, stringValue, index);
    }

    public object DeepCopy(object value)
    {
        return value;
    }

    public object Replace(object original, object target, object owner)
    {
        return original;
    }

    public object Assemble(object cached, object owner)
    {
        return cached;
    }

    public object Disassemble(object value)
    {
        return value;
    }

    public SqlType[] SqlTypes
    {
        get { return new[] {NHibernateUtil.String.SqlType}; }
    }

    public Type ReturnedType 
    {
        get { return typeof (TEnum); }
    }

    public bool IsMutable 
    {
         get { return false; }
    }
}

これを派生した、目的の enum 用のクラスを作ればOKです。

public class GenderStringType : EnumStringType<Gender>
{
}

テーブル

あとはテーブルの列の型を文字列に変更して...

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] NVARCHAR (10)  NOT NULL,
    CHECK ([Gender] = ‘Male‘ OR [Gender] = ‘Female‘)
);

マッピング

マッピングに利用する UserType を指定してあげます。

<class name="Student" table="Students">

    <id name="Id">
        <generator class="identity"></generator>
    </id>

    <property name="Name"></property>
    <property name="Gender" type="Sample.NH.UserTypes.GenderStringType, Sample.NH"></property>

</class>

これで enum を文字列で永続化できます。

補足

例では簡潔にするために省きましたが、整数/文字列どちらで保存する場合も、 実際にはマスタテーブル(この例だと Genders とか)を作って外部キー制約を指定しておいたほうがいいですね。

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] NVARCHAR (10)  NOT NULL,
    FOREIGN KEY (Gender) REFERENCES Genders (Value) 
);
CREATE TABLE [dbo].[Genders] (
    [Id]     INT            NOT NULL PRIMARY KEY,
    [Value]   NVARCHAR (10) NOT NULL,
    UNIQUE (Value)
);

2013年1月11日金曜日

Jenkinsでのビルド時にNuGetパッケージを復元する

NuGet でパッケージ管理をしているプロジェクトを Jenkins でビルドしようとするとうまくいきませんでした。その解決方法をメモします。

うまくいかない理由

NuGet のパッケージは通常バージョン管理下に置かれません。
なのでソースコードをチェックアウトした段階ではパッケージがなく、ビルド時にパッケージを復元しないといけないことになります。

NuGetパッケージの復元を有効化

Visual Studio からパッケージの復元を設定できます。
ソリューションエクスプローラから NuGet パッケージの復元を設定できるので有効にしてあげます。

ソリューションを右クリックして、「NuGetパッケージの復元の有効化」を選択します。

こんな感じの確認ダイアログが出るのでOKします。

少し待つとソリューションフォルダが作成されてこんなファイルが追加されます。
これらを使ってビルド時にパッケージを復元してくれます。

Jenkinsでビルドしてみる

これでバッチリ!ということで Jenkins でビルドすると、今度はこんなエラーになります。

パッケージの復元は既定で無効になっています。確認のため、Visual Studio の [オプション] ダイアログ ボックスを開き、Package Manager ノードをクリックして、[NuGet がビルド中に存在しないパッケージをダウンロードするのを許可する] チェック ボックスをオンにします。また、環境変数 'EnableNuGetPackageRestore' を true に設定して確認することもできます。

メッセージの通りではありますが、ソリューションで復元を有効にするだけでは復元を実行してくれません。
サーバの環境変数「EnableNuGetPackageRestore」を true に設定すると復元してくれるようになります。

これで解決

これでパッケージが復元され、無事ビルドを行うことができます。

おまけ: アンオフィシャルなパッケージソース

NuGet公式サイトにて配布されいるパッケージは以上の通りでOKですが、もし自前の NuGet サーバを利用して、そちらからインストールしているパッケージがある場合、もうひと手間必要です。
※ 私は社内に NuGet サーバを立ててパッケージ管理していたのでちょっとハマりました。

「NuGetパッケージの復元の有効化」したときに追加された「NuGet.targets」を開きます。このファイル中に以下のような部分があります。

    <ItemGroup Condition=" '$(PackageSources)' == '' ">
        <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
        <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
        <!--
            <PackageSource Include="https://nuget.org/api/v2/" />
            <PackageSource Include="https://my-nuget-source/nuget/" />
        -->
    </ItemGroup>

丁寧なコメントの通りですが、自前の NuGet サーバを利用したい場合は、コメントアウトしてある箇所をコメントインして、パッケージソースを指定してやる必要があります。

    <ItemGroup Condition=" '$(PackageSources)' == '' ">
        <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
        <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
        <PackageSource Include="https://nuget.org/api/v2/" />
        <PackageSource Include="https://my-nuget-source/nuget/" />
    </ItemGroup>

「https://my-nuget-source/nuget/」の部分を自前のパッケージソースに変えてあげればOKですね。

2012年12月15日土曜日

[C#]式木とLINQ

式木がどんな風に役に立っているか、みんな大好き LINQ で見ていきたいと思います。
普段 LINQ はモリモリ使っていますが、その中身はボンヤリとしか理解していなかったのでおさらいしてみました。

LINQと式木の概要

例として LINQ to SQL を挙げてみていきます。LINQ to SQL で以下のようなクエリ式を書いた場合、

IQueryable<Student> query =
    from
        student in db.Student
    where
        student.Gender == "Male" 
    select
        student
    ;

db.Student を foreach して Genderプロパティ が "Male" の項目を探す... ではなく。
ご存知の通り実際にはこんな感じのSQL文が生成され、ADO.NET によって実行されます。

SELECT 
    [t0].[Id], 
    [t0].[Name], 
    [t0].[Gender]
FROM 
    [dbo].[Students] AS [t0]
WHERE 
    [t0].[Gender] = @p0
-- @p0 = 'Male'

このSQL文を作る過程に式木が利用されています。まず、上記のクエリ式は実際にはシンタックスシュガーで、以下の様なメソッド式に変換されます。

db.Student.Where(student => student.Gender == "Male");

さらにWhereメソッドは拡張メソッドなので、こうなりますね。

Where(db.Student, student => student.Gender == "Male");

この式がこんな感じの式木として扱われます。

同等の Expression を組み立てるとこんな感じです。

var whereMethod = typeof (Queryable)
    .GetMethods()
    .First(m => m.Name == "Where")
    .MakeGenericMethod(new Type[] { typeof(Student) });

var paramStudent = Expression.Parameter(typeof (Student), "student");

var expression =
    Expression.Call(whereMethod,
        Expression.Constant(db.Student),
        Expression.Lambda<Func<Student, bool>>(
            Expression.Equal(
                Expression.Property(paramStudent, "Gender"),
                Expression.Constant("Male")
                ),
            paramStudent
            )
        )
        ;

LINQ to SQL では結果を取得する際、まず式木を評価・解析することで適切なSQL文を作成してくれています。

LINQ to XXXXX

式自体(式木)を評価することで、LINQ to SQL や LINQ to Entities では式を SQL に変換しますが、LINQ to XML, LINQ to DataSet, LINQ to NHibernate ... などでは式を各種データソースに適切なアクセス方法に変換しています。

このような仕組みのおかげで統一的な式で、各種データソースへのアクセス、操作をうまく統合しています。
これが LINQ (統合言語クエリ) と呼ばれるゆえんであり、式木を最も活用している例でもあります。

応用してみる(?)

例えば LINQ to SQL では一括の UPDATE や DELETE ができません。例えば先程の例で 性別(Gender) が 男(Male) の生徒を全て削除しようと思うと以下のようになります。

var query =
    from
        student in db.Student
    where
        student.Gender == "Male"
    select
        student
    ;

db.Student.DeleteAllOnSubmit(query);
db.SubmitChanges();

この操作は以下の様な SQL を発行します。まず一度 Gender = 'Male' な生徒を全て取得してから...

SELECT 
    [t0].[Id], 
    [t0].[Name], 
    [t0].[Gender]
FROM 
    [dbo].[Students] AS [t0]
WHERE 
    [t0].[Gender] = @p0
-- @p0 = 'Male'

それぞれ1つずつ DELETE句 を発行していきます。

DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 1
DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 2
DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 3
...

これはあまりスマートとはいえないやり方ですね。本来以下のような SQL を発行すべきです。

DELETE FROM [dbo].[Students] WHERE [Gender] = @p0 -- @p0 = 'Male'

このように LINQ to SQL は更新や削除の操作がちょっと弱いです。しかしこの問題に対しては、自前の拡張メソッドを作って上記のような適切な SQL を発行するように実装することで対処できます。
以下の記事が大変参考になりますので、是非ご覧になってみて下さい。

もうちょっと具体的には

今回ご紹介した LINQ の仕組みは、IQueryableインターフェイス 及び IQueryProviderインターフェイス の組み合わせで実現されています。
以下の記事が大変わかり易く参考になりますので、紹介させて頂きます。

IQueryable と IQueryProvider の仕組みが把握できると、独自の LINQ to XXXXX を作ることができて夢が広がりますね。

さいごに

LINQ初心者の方や、式木がよくわからないぜ... という方の参考になれば幸いです。

※ 余談ですが http://ufcpp.net/ さんの C# の情報の充実度はハンパじゃないですね... C# について何か調べると必ずといっていいほど検索で挙がってきます。MSDNよりわかりやすい事が多く、いつも参考にさせて頂いています。

2012年12月4日火曜日

[C#]dynamicと拡張メソッド

ひょっとして dynamic って拡張メソッドもイケるのかな?とふと思ったので試してみました。

こんな感じの適当なクラスと拡張メソッドを用意します。

class Hoge
{
    public string Method()
    {
        return "Hoge's instance method.";
    }
}

static class HogeHelper
{
    public static string ExtensionMethod(this Hoge hoge)
    {
        return "Hoge's extension method.";
    }
}

まずは普通にインスタンスメソッドを dynamic から呼び出します。

var hoge = new Hoge();
dynamic dynamicHoge = hoge;

System.Console.WriteLine(dynamicHoge.Method());
 

結果はこんな感じで普通です。

Hoge's instance method.
 

では本題のこうしたときです。

System.Console.WriteLine(dynamicHoge.ExtensionMethod());

拡張メソッドは単なるシンタックスシュガーなので、

hoge.ExtensionMethod();

このような拡張メソッドの利用は、

HogeHelper.ExtensionMethod(hoge);

こんな風に展開されて実行されます。
なので、Hoge に ExtensionMethod というメソッドがあるわけではなく、dynamic はバインドできないはずです。

さっきのコードを実行してみます。

System.Console.WriteLine(dynamicHoge.ExtensionMethod());
Runtime Binder Exception: 'Hoge' に 'ExtensionMethod' の定義がありません

はい、予想通りでした。もしかしたら拡張メソッドもバインドできてしまうのか...!? と思ったのですが、そんなことはありませんでした。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...