特定のDBに依存しないデータアクセス方法(DbProviderFactories)

C#で特定のDBに依存しないデータアクセスをする場合の実装方法です。

例えば、Oracleに接続する場合はOracleConnectionを使うし、PostgreSqlに接続する場合はNpgSqlを使用すると思いますが、将来的に切り替えたりさまざまなプロジェクトで流用していこうという場合に依存しない書き方が必要になっています。

やり方としては、DbProviderFactoriesとSystem.Data.Common.DbConnectionなどの基本クラスのみを使って実装する方法です。汎用的なデータアクセスライブラリの書き方としてはお勧めの方法だと思います。

まず、事前準備としてapp.configにファクトリー用の設定をいれます。














DbProviderFactoriesの中身がそうです。typeの中身に使用するライブラリのFactoryクラスを指定します。あとは、以下のような感じでアクセスできるようになります。利用するのはSystem.Data.Common以下の基本クラスのみを使ってアクセスします。


using System;
using System.Data;
using System.Data.Common;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
System.Data.Common.DbConnection conn = DbProviderFactories.GetFactory("Npgsql").CreateConnection();
conn.ConnectionString = Properties.Settings.Default.ConnectionString;
conn.Open();

var command = conn.CreateCommand();
command.CommandText = "select current_timestamp";
command.CommandType = CommandType.Text;
command.Prepare();

var reader = command.ExecuteReader();
var table = new DataTable();
table.Load(reader);

conn.Close();

this.Text = table.Rows[0][0].ToString();

}

}
}

プロジェクトファイルが勝手にチェックアウト状態になる

ウェブアプリケーションを開発している際に、何も変更していないのにプロジェクトファイルがチェックアウト状態になることがあります。


原因は、プロジェクトプロパティの[Web]タブの設定です。



・[すべてのユーザにサーバ設定を適用(プロジェクトファイルに格納)]がオン。
・[Visual Studio 開発サーバを使用する(D)]がオン。
・[ポートの自動割り当て]がオン。



となっている環境で発生します。これは自動割り当てされたポートがプロジェクトファイルに書き込まれるためです。VisualStudioが編集のためにTFSやVSSなどのソースコード管理システムから自動チェックアウトします。


対応方法は、上記のうち[すべてのユーザにサーバ設定を適用(プロジェクトファイルに格納)]をオフにしておきます。開発時に利用するウェブサーバの設定がすべてプロジェクトファイルではなく、ユーザファイル(**.csproj.user)に書き込まれるようになります。ユーザファイルはソースコード管理対象外ですから、変更もしていないのにチェックアウト状態になることはなくなります。



★★★★★★★★★★★★★★★★★★★★★★



また、開発用ウェブサーバのポート割り当てを固定にしている場合(IIS Expressなど。最近はこちらがデフォルトですね。)だと、ターミナルサービス(RDPセッション)環境ではポート番号が重複しデバッグが出来なくなります。(VisualStudioがデバッグ実行直後、終了する。)



・[すべてのユーザにサーバ設定を適用(プロジェクトファイルに格納)]はオフにしておいた方が無難です。


PS:プロジェクト設定ついでに、デバッガーセクションの[エディットコンティニュを有効にする(B)]もオンにしておきましょう。

Visual Studio 2012 でのプロジェクトプロパティで変更しておいた方が良い箇所

デバッグ中にコードを修正するために必要な設定


[ビルド]-[プラットフォームターゲット]→x86
[Web]-[デバッガ]→[エディットコンティニュを有効にする]→オン



・RDPセッションホスト(ターミナルサービス)環境で複数ユーザでデバッグするために必須


[Web]-[サーバー]→[すべてのユーザーにサーバー設定を適用(プロジェクトファイルに格納)]→オフ


ソースコード管理ツール(VSS,TFSなど)を使っていない場合は関係ありません。

FirstChanceExceptionが無限ループに陥らないようにする実装方法

.Net 4.0 から導入された FirstChanceException イベントハンドラは、例外が発生した際に最初に例外が通知されるイベントです。(try catch よりも先に。)


但し、注意点としてFirstChanceExceptionイベントハンドラの中で新たに例外が発生するとFirstChanceExceptionが再帰的に呼び出されスタックオーバーフローしてしまうという問題があります。MSDNにはこの問題に対する対処としてCER(制約された実行領域)を使うことを推奨していますが、CERであっても例外が発生すれば再帰呼び出しされるので根本的な問題解決にはならないと思います。もちろん、CERなのでOutOfMemoryExceptionなどは防げると思います。


根本的な正しいアプローチとしては無限に再帰呼び出ししないようにすること。つまりFirstChanceException イベントハンドラを解除することです。


★★★★★★★★★★★★★★★★★★★★


まとめ


基本的には次の2つの対処を行えばよいと思います。

  1. 例外が発生しないようなコードを書くこと(大前提)
  2. FirstChanceExceptionのコードを実行する際にFirstChanceExceptionイベントハンドラの登録を解除すること。
using System;
using System.Windows.Forms;
using System.Diagnostics;

namespace WindowsFormsApplication1
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
      AppDomain.CurrentDomain.FirstChanceException += FirstChanceExHandler;
    }

    private void FirstChanceExHandler(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
    {

      try
      {
        AppDomain.CurrentDomain.FirstChanceException -= FirstChanceExHandler;

        try
        {
          throw new Exception("想定していない例外");
        }
        catch(Exception ex)
        {
          System.Diagnostics.Debug.WriteLine(ex.ToString());
        }

      }
      finally
      {
        AppDomain.CurrentDomain.FirstChanceException += FirstChanceExHandler;
      }

    }

  }
}

Windows 8 でスタートメニューを表示する

Windows 8 からスタートメニューが無くなりましたが、早速、スタートメニューを表示するアプリをリリースしている人がいたので使わせてもらうことに。


Lee-Soft ViStart


海外のソフトウェアだが日本語も表示されるし見た目もWindows 7 なので使いやすい。

IIS でワーカープロセスのリサイクルが発生した際にイベントログを記録する

IISのワーカープロセスは、不安定にならないよう定期的な時間間隔(1740分ごと)に再起動するように設定されています。この再起動処理をIISはリサイクルと呼んでいます。既定では時間間隔(1740分毎)ですが、リクエストの数(1000万リクエスト毎など)や特定の時間(毎朝03:00)、メモリの使用量などに応じてリサイクルイベントを発生させるよう設定することができます。


リサイクル自体は問題ありませんが、どういったタイミングで起きているかをイベントログで記録することができます。IIS6の既定ではログを記録しないこと、IIS7の既定では一部ログを記録するとなっています。イベントログを出すかどうか調整します。

  • IIS6の場合

既定ではリサイクルイベントのログを記録しないので必要なリサイクルイベントを記録するよう設定します。


管理者権限で、inetpub\adminscriptsにある adsutil.vbs を使用します。コマンドは、cscript adsutil.vbs set w3svc/AppPools/[アプリケーションプール名]/[リサイクルイベント名] trueになります。


リサイクルイベントは次の8つ。


AppPoolRecycleTime

処理時間が上限に達した場合(1740分など)。イベントID(1074)

AppPoolRecycleRequests

受け付けたリクエスト数が上限に達した場合(100万リクエストなど)。イベントID(1075)

AppPoolRecycleSchedule

スケジュールされた時刻になった場合(AM 03:00など)。イベントID(1076)

AppPoolRecycleMemory

プロセスの仮想メモリが上限に達した場合。イベントID(1077)

AppPoolRecycleIsapiUnhealthy

ISAPIワーカープロセスに異常が発生した場合。イベントID(1078)

AppPoolRecycleOnDemand

管理者によってワーカープロセスをリサイクルするよう要求があった場合。イベントID(1079)

AppPoolRecycleConfigChange

アプリケーションプールの設定変更によりプロセスが再起動される場合。イベントID(1080)

AppPoolRecyclePrivateMemory

プロセスのプライベートメモリが上限に達した場合。イベントID(1177)


コマンドラインからの実行例です。

C:\>cd inetpub\adminscripts

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleTime true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleTime : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleRequests true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleRequests : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleSchedule true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleSchedule : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleMemory true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleMemory : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleIsapiUnhealthy true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleIsapiUnhealthy : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleOnDemand true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleOnDemand : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecycleConfigChange true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecycleConfigChange : (BOOLEAN) True

C:\Inetpub\AdminScripts>cscript adsutil.vbs set w3svc/AppPools/DefaultAppPool/AppPoolRecyclePrivateMemory true
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

AppPoolRecyclePrivateMemory : (BOOLEAN) True


  • IIS7の場合

既定ではプライベートメモリ制限、仮想メモリ制限、定期的な間隔のリサイクルイベントの3つを記録します。他に必要なイベントがあれば記録します。IISマネージャでアプリケーションプールの詳細設定から設定を変更できます。


  • まとめ

リサイクルイベントのログは最初は全部記録するようにして、イベントログを眺めながら不要であれば外すという運用でよいと思います。異常な理由で、リサイクルが頻繁に発生するようなら何らかの対策を立てる必要があります。

例外の処理コストとパフォーマンスチューニング

例外に掛かる処理コストについてint.Parseとint.TryParseを使って比較調査を行ってみました。


入力された文字が数字か否かを判断する方法として、数値ではない場合に例外を発生させるint.Parseがあります。

    private bool IsNumericByException(string test)
    {

      try
      {
        int.Parse(test);
      }
      catch (FormatException ex)
      {
        return false;
      }

      return true;

    }


もうひとつ別のバージョンとして、数値ではない場合にFalseを返すint.TryParseがあります。

    private bool IsNumericByTryParse(string test)
    {
      int a;
      return int.TryParse(test, out a);
    }

この2つの処理をそれぞれ10万回呼び出すと、int.Parseが6535ミリ秒、int.TryParseが9ミリ秒という結果に。



700倍くらいの速度差がある計算になります。比率で言えばそういうレベルですが10万回例外を発生させてもその程度です。例外あたり0.6ミリ秒なので、問題視することもなさそうです。


もし、この箇所をチューニングしたいと考えるなら、AppDomainにFirstChanceExceptionイベントハンドラを登録して、どういった例外が多く発生しているのか分析すると頻度の多い例外に焦点をあてることができます。問題の例外をTryParseのような例外が発生しない形式に置き換えることでパフォーマンスを向上させることができます。int.Parseだと、1例外あたり0.6ミリ秒程度ですが。(もちろん、これは環境に拠ります)


検証コード

using System;
using System.Windows.Forms;
using System.Diagnostics;

namespace WindowsFormsApplication1
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {

      AppDomain.CurrentDomain.FirstChanceException += FirstChanceExHandler;

      const int loop = 100000;

      Stopwatch sw = new Stopwatch();

      sw.Start();

      for (int i = 0; i < loop; i++)
      {
        IsNumericByException("hoge");
      }

      sw.Stop();

      label1.Text = sw.ElapsedMilliseconds.ToString();

      sw.Reset();

      sw.Start();

      for (int i = 0; i < loop; i++)
      {
        IsNumericByTryParse("hoge");
      }

      sw.Stop();

      label2.Text = sw.ElapsedMilliseconds.ToString();

    }

    private bool IsNumericByException(string test)
    {

      try
      {
        int.Parse(test);
      }
      catch (FormatException ex)
      {
        return false;
      }

      return true;

    }

    private bool IsNumericByTryParse(string test)
    {
      int a;
      return int.TryParse(test, out a);
    }

    private void FirstChanceExHandler(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
    {

      StackTrace st = new StackTrace(e.Exception);
      System.IO.File.AppendAllText("exception.txt", e.Exception.Message + e.Exception.GetType().FullName + st.ToString() + "\n");

    }


  }
}