かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

EnterpriseLibrary 6のSemantic Logging Application Blockの感想

過去分

既存のロギングライブラリとの個人的な比較

Semantic Logging Application Blockを触ってみた主観的な感想になります。

ログAPIの独自実装が前提

EventSourceを継承した独自のタイプセーフなログAPIの実装が必須になります。これは、ぱっと使うには少しだけめんどくさいところではありますが、汎用的なログAPIをラップして業務アプリに特化したログAPIを作ることって結構あると思います。それなら、はじめからEventSourceクラスを継承して独自APIを作ることを前提としているSemantic Logging Application Blockって、現実的なのではないかと思います。

ログのAPI

独自実装なので、好きにできます。はい。

実装のときも、固定のパラメータはアトリビュートで指定できたりと、楽できる仕組みがそろっています。

ログ出力の設定

一般的なログイングAPIでは、構成ファイルにログの設定を記述します。これはこれでいいと思うのですが、構成ファイルで決められた内容しか制御できません。Semantic Logging Application Blockでは、ログがIOとして上がってくるので、Rxを使って柔軟にフィルタリングが出来ます。これ最高に強みだと思います。コードで柔軟に設定するのって個人的には好きです。

その他カスタマイズ可能な部分

一般的なログイングライブラリでカスタマイズ可能な、出力先やフォーマットなどは一通り独自クラスを定義することで簡単に拡張可能なようになっています。ここら辺は、普通のライブラリと一緒ですね。

まとめ

ということで、とっつきにくいと感じてたSemantic Logging Application Blockは、触ってみると意外といいやつなことがわかりました。ライブラリ好きに選んでいいという.NETの話しがあったら、ちょっと使ってみようかなと思います。

EnterpriseLibrary 6のValidation Application Blockを触ってみた

恐らく、一番機能が豊富だと思われるオブジェクトの検証機能を持ったライブラリです。標準のDataAnnotationsよりも、機能は多い(ともに使うこともできる)です。

個人的にプロパティに属性を追加して使うのが好みなので、その使い方について紹介したいと思います。

使い方

NuGetでEnterpriseLibrary Validationあたりで検索して"EnterpriseLibrary - Validation Application Block"を追加します。

Microsoft.Practices.EnterpriseLibrary.Validation.Validators名前空間にある各種****ValidatorAttributeを使ってプロパティに検証の条件を追加します。例えば名前が必須入力で、1~10文字の間の場合は以下のように定義します。

public class Person
{
    [NotNullValidator(MessageTemplate = "名前を入力してください")]
    [StringLengthValidator(1, 10, MessageTemplate = "名前は1~10文字です")]
    public string Name { get; set; }
}

バリデーションを行うには以下のようなコードになります。

// バリデーターを作成する
var validator = ValidationFactory.CreateValidator<Person>();
// 引数で渡されたオブジェクトの検証
var validationResults = validator.Validate(new Person());

// 検証結果を確認
Console.WriteLine(validationResults.IsValid); // False

// OKなケース
var validationResults2 = validator.Validate(new Person { Name = "tanaka" });
Console.WriteLine(validationResults2.IsValid); // True

プロパティを増やして、属性をつけることでValidateメソッドで全プロパティの検証結果がValidationResultsという形で返ってきます。

込み入ったオブジェクトの検証

これが、ほかの奴であまり見ない機能です。例えば、先ほどのPersonクラスの名前をFirstNameとLastNameを持ったオブジェクトにしてみます。

// 名前を表すオブジェクト
public class NameObject
{
    [NotNullValidator(MessageTemplate = "FirstNameは必須です")]
    public string FirstName { get; set; }

    [NotNullValidator(MessageTemplate = "LastNameは必須です")]
    public string LastName { get; set; }
}

// NameObjectを持つオブジェクト
public class Person
{
    [NotNullValidator(MessageTemplate = "名前は必須です")]
    // ObjectValidatorをつけるとオブジェクトのプロパティも妥当性検証の対象になる
    [ObjectValidator]
    public NameObject Name { get; set; }
}

ObjectValidatorをつけると、NameObjectの中も妥当性検証を行ってくれるようになります。

// バリデーターを作成する
var validator = ValidationFactory.CreateValidator<Person>();
// 引数で渡されたオブジェクトの検証
var validationResults = validator.Validate(new Person());

// 検証結果を確認
Console.WriteLine(validationResults.IsValid); // False

// OKなケース
var validationResults2 = validator.Validate(new Person 
{ 
    Name = new NameObject 
    { 
        FirstName = "taro", 
        LastName = "tanaka" 
    } 
});
Console.WriteLine(validationResults2.IsValid); // True

エラー情報を取得する

エラーの情報の取得はValidationResultsにコレクションとして入ってるのでループ回したりしてとることができます。例えば列挙する場合は以下のようになります。

// バリデーターを作成する
var validator = ValidationFactory.CreateValidator<Person>();
// 引数で渡されたオブジェクトの検証
var validationResults = validator.Validate(new Person { Name = new NameObject() });

// 検証結果を確認
Console.WriteLine(validationResults.IsValid); // False

foreach (var r in validationResults)
{
    Console.WriteLine("プロパティ {0}: メッセージ {1}", r.Key, r.Message);
}

実行すると以下のようになります。

プロパティ FirstName: メッセージ FirstNameは必須です
プロパティ LastName: メッセージ LastNameは必須です

これは、全部のプロパティのエラーが入ってるので、特定のプロパティのみ検証したい場合は、PropertyValueValidatorを使います。こいつは属性じゃなくて、手組で検証ルールを組まないといけないです。例えばNullじゃなくてオブジェクトの検証をやる場合は以下のようになります。

var propertyValidator = new PropertyValueValidator<Person>("Name", 
    new AndCompositeValidator(
        new NotNullValidator(),
        new ObjectValidator())
    );
// 引数で渡されたオブジェクトの検証
var validationResults = propertyValidator.Validate(new Person { Name = new NameObject() });

// 検証結果を確認
Console.WriteLine(validationResults.IsValid); // False

foreach (var r in validationResults)
{
    Console.WriteLine("プロパティ {0}: メッセージ {1}", r.Key, r.Message);
}

これでNameプロパティのみ検証が行われます。

プロパティにつけた属性を有効活用したい場合は、全体を検証したあとLINQあたりでしぼったりしないといけないと思います。

// バリデーターを作成する
var validator = ValidationFactory.CreateValidator<Person>();
// 引数で渡されたオブジェクトの検証
var validationResults = validator.Validate(new Person { Name = new NameObject() });

// 検証結果を確認
Console.WriteLine(validationResults.IsValid); // False

var result = validationResults.FirstOrDefault(r => r.Key == "Name");
if (result == null)
{
    // エラーがない
}
else
{
    // エラーがある
}

まとめ

とまぁ走り書きですが、結構豊富な機能があるんじゃないんでしょうか。

Enterprise Library 6のSemantic Logging Application Blockで独自の出力先に出力する方法

過去分

はじめに

ログ関係のライブラリを使いこんでいくうちに出てくる要求として出力先をカスタマイズしたいというのはよくあると思います。Semantic Logging Application Blockでも当然そこは拡張できるように作られてるのでやってみました。

****Sinkというクラスを作る

Semantic Logging Application BlockはRxと相性がいいだけあって、ログの出力する人はEventEntryのIObserverです。OnNextを処理すればOKというわかりやすい設計。今回はコンソールに出すだけの簡単なものを作ってみました。フォーマッティングはIEventTextFormatterにお任せしてます。

// 独自の出力先
class MyConsoleSink : IObserver<EventEntry>
{
    private IEventTextFormatter formatter;
    public MyConsoleSink(IEventTextFormatter formatter = null)
    {
        this.formatter = formatter ?? new EventTextFormatter();
    }

    public void OnCompleted()
    {
    }

    public void OnError(Exception error)
    {
    }

    public void OnNext(EventEntry value)
    {
        // 値がわたってきたときだけ
        if (value == null)
        {
            return;
        }

        // formatterで整形して出力する
        using (var w = new StringWriter())
        {
            this.formatter.WriteEvent(value, w);
            Console.Write(w);
        }
    }
}

そして、IObservableと接続するための拡張メソッドを定義します。LogTo****という名前で作るのが一般的っぽいです。

// MyConsoleSink購読用拡張メソッド
static class MyConsoleSinkExtensions
{
    public static SinkSubscription<MyConsoleSink> LogToMyConsole(this IObservable<EventEntry> self, IEventTextFormatter formatter = null)
    {
        var sink = new MyConsoleSink(formatter);
        var d = self.Subscribe(sink);
        return new SinkSubscription<MyConsoleSink>(d, sink);
    }
}

Subscribeして、SinkSubscriptionというものを返すのがお約束っぽいのでそれに従ってます。Semantic Logging Application BlockのログがIObservableから発行されたものをIObserverで監視して出力するだけということがわかれば、間にRxのLINQ挟み込めるのも納得ですね。

使い方

// リスナー作って
var l = new ObservableEventListener();
// 監視するログの種類を設定して
l.EnableEvents(
    MyEventSource.Log,
    EventLevel.Verbose,
    MyEventSource.Keywords.Diagnostic | MyEventSource.Keywords.Lifecycle);
// 自前のログ出力先へ出力する
l.LogToMyConsole();

MyEventSource.Log.Start();
MyEventSource.Log.Query("select * from dual");
MyEventSource.Log.Stop();

とても簡単ですね。

Enterprise Library 6のSemantic Logging Application Block + Reactive Extensions

Semantic Logging Application Blockのログですが、こいつはRxを使ってフィルタリングとかが出来ます。例えば、これまで作ってきたやつでInformation以上のログだけ表示するようにするには以下のような感じ。

// リスナー作って
var l = new ObservableEventListener();
// 監視するログの種類を設定して
l.EnableEvents(
    MyEventSource.Log,
    EventLevel.Verbose,
    MyEventSource.Keywords.Diagnostic | MyEventSource.Keywords.Lifecycle);
// LINQで色々できる!
l.Where(e => e.Schema.Level <= EventLevel.Informational)
    .LogToConsole();

まぁこの例だとEnableEventsでInformation渡せば済む話ですが…。ドキュメントには指定した条件でフラッシュするコードが例示されてました。引用しておきます。

public static IObservable<T> FlushOnTrigger<T>(
    this IObservable<T> stream, Func<T, bool> shouldFlush, int bufferSize) {  
    return Observable.Create<T>(observer =>
    {
        var buffer = new CircularBuffer<T>(bufferSize);
        var subscription = stream.Subscribe(newItem =>
            {
               if (shouldFlush(newItem))
               {
                   foreach (var buffered in buffer.TakeAll())
                   {
                       observer.OnNext(buffered);
                   }
                   observer.OnNext(newItem);
               }
               else
               {
                   buffer.Add(newItem);
               }
            },
            observer.OnError,
            observer.OnCompleted);
        return subscription;  
    });
}

引数でわたしたデリゲートがTrueになったタイミングでログの内容を後続に渡す感じですね。

var listener = new ObservableEventListener(); 
listener.EnableEvents(MyCompanyEventSource.Log,
    EventLevel.Informational,
    Keywords.All); 
listener.FlushOnTrigger(entry => entry.Schema.Level <= EventLevel.Error, bufferSize: 10)
    .LogToConsole();

こんな風に使うとエラーのあった直近10個のログだけを出すという、なんともありがたい感じになってます。これはいい・・・!(ドキュメントのコピーしただけですけど)

このほかには、指定した範囲の重要度とかだけに絞るとか1つのListenerから色々分派させることができそうです。

EnterpriseLibrary 6のSemantic Logging Application BlockでAzure Storageにログをはく

先日触ってみたSemantic Logging Application Blockですが、ちょっと手を加えるだけでAzureのStorageやSQL Serverにログが吐き出せます。今回は、AzureのStorageに吐き出してみたいと思います。

Azureに適当にストレージを作ったらVisual Studioのサーバーエクスプローラから接続文字列を取得しておきます。そして、NuGetでEnterpriseLibrary Semantic Azureあたりで検索して「Semantic Logginc Application Block - Azure Sink」を追加します。

前回のプログラムのLogToConsoleをLogToWindowsAzureTableに変えます。引数のインスタンス名は適当でOKで、第二引数に接続文字列を渡します。

プログラムはこんな感じ。

// リスナー作って
var l = new ObservableEventListener();
// 監視するログの種類を設定して
l.EnableEvents(
    MyEventSource.Log,
    EventLevel.Verbose,
    MyEventSource.Keywords.Diagnostic | MyEventSource.Keywords.Lifecycle);
// Windows Azure Storage ServiceのTableに出力する
l.LogToWindowsAzureTable("インスタンス名",
    // 接続文字列
    "DefaultEndpointsProtocol=https;AccountName=semanticsample;" +
    "AccountKey=IG4TJGmpV7Mt+VSwPACIuiVIYLrZQXyI9y98/m2O/excbpS6+GLNUDl2MyfB6K3z28IlevlTT7O/difd/ZGs/g==");

MyEventSource.Log.Start();
for (int i = 0; i < 1000; i++)
{
    MyEventSource.Log.Query("select * from dual");
    Thread.Sleep(10);
}
MyEventSource.Log.Stop();

実行すると、Azureのストレージのテーブルにちゃんとログが出てるのが確認できます。(クライアントでバッファリングしてるので、最後のほうのログは抜け落ちてます。今回みたいな単発Exeには向かないのでWebアプリとかで使うのがよさそうですね)

f:id:okazuki:20140713111944j:plain

EnterpriseLibrary 6のSemantic Logging Application Blockを触ってみた

Logging Application Blockというログのがあるにも関わらず、Semantic Logging Application Blockというのが追加されてます。こいつはタイプセーフにログのAPIが作れて、いい感じだぜ?みたいなノリっぽいけど、ちゃんとドキュメント読んでないのでよくわかりません。

因みにETW(Event Tracing for Windows)(イベントログじゃないYO)を裏で使うみたいです。ETWについては、以下のBlogが詳しい感じです。因みにわたしはETWが何なのかよく知りません。

ログAPIの作り方

これは、Semantic Logging Application Blockの機能というよりは.NET 4.5から追加されたEventSourceクラスの使い方になります。

こいつをちゃんと使うと、タイプセーフなロギングAPIができるって寸法ですね。例えばこんな感じ。

[EventSource(Name = "MyEventSource")]
class MyEventSource : EventSource
{
    /// <summary>
    /// キーワード
    /// </summary>
    public static class Keywords
    {
        public const EventKeywords Diagnostic = (EventKeywords)1;
        public const EventKeywords Lifecycle = (EventKeywords)2;
    }

    /// <summary>
    /// タスク
    /// </summary>
    public static class Tasks
    {
        public const EventTask DB = (EventTask)1;
        public const EventTask App = (EventTask)2;
    }

    private MyEventSource() { }

    // シングルトンのインスタンス
    private static MyEventSource instance = new MyEventSource();

    public static MyEventSource Log { get { return instance; } }

    // 各種ログAPI
    [Event(1, Keywords = Keywords.Lifecycle, Level = EventLevel.Informational,
        Message = "Process start", Opcode = EventOpcode.Start, Task = Tasks.App)]
    public void Start()
    {
        this.WriteEvent(1);
    }

    [Event(2, Keywords = Keywords.Lifecycle, Level = EventLevel.Informational,
        Message = "Process stop", Opcode = EventOpcode.Stop, Task = Tasks.App)]
    public void Stop()
    {
        this.WriteEvent(2);
    }

    [Event(3, Keywords = Keywords.Diagnostic, Level = EventLevel.Verbose,
        Message = "Execute qyeru {0}", Opcode = EventOpcode.Info, Task = Tasks.DB)]
    public void Query(string query)
    {
        this.WriteEvent(3, query);
    }
}

こいつで、以下のように使うと、ETWにログが出るっていう寸法です。

MyEventSource.Log.Start();
MyEventSource.Log.Query("select * from dual");
MyEventSource.Log.Stop();

Semantic Logging Application Blockを使う

ETWのに出たログを監視して、いろんなところに出してくれるのがSemantic Logging Application Blockみたいです。NuGetでEnterpriseLibrary Semanticあたりで検索してSemantic Logging Application Blockを追加します。

そして、アプリケーションの最初の初期化処理あたりで以下のようなコードを書きます。

// リスナー作って
var l = new ObservableEventListener();
// 監視するログの種類を設定して
l.EnableEvents(
    MyEventSource.Log,
    EventLevel.Verbose,
    MyEventSource.Keywords.Diagnostic | MyEventSource.Keywords.Lifecycle);
// 何処にログを出すか決める
l.LogToConsole();

こうすると、先ほどのコードで出力したログがETWを経由してコンソールに出るようになります。実行すると以下のような感じのログが出ます。

EventId : 1, Level : Informational, Message : Process start, Payload : , EventName : AppStart, Timestamp : 2014-07-12T14:56:50.0105184Z, ProcessId : 9736, ThreadId : 8384

EventId : 3, Level : Verbose, Message : Execute qyeru select * from dual, Payload : [query : select * from dual] , EventName : DBInfo, Timestamp : 2014-07-12T14:56:50.0265193Z, ProcessId : 9736, ThreadId : 8384

EventId : 2, Level : Informational, Message : Process stop, Payload : , EventName : AppStop, Timestamp : 2014-07-12T14:56:50.0265193Z, ProcessId : 9736, ThreadId : 8384

LogToConsoleには、どのような形式で出力するか設定するフォーマッターも設定できて、JSON形式なんかで出すこともできます。

l.LogToConsole(new JsonEventTextFormatter());

出力が以下のようになります。

{"ProviderId":"8983a2e6-c5d2-5a1f-691f-db243cb1f681","EventId":1,"Keywords":2,"L
evel":4,"Message":"Process start","Opcode":1,"Task":2,"Version":0,"Payload":{},"
EventName":"AppStart","Timestamp":"2014-07-12T14:58:30.2691727Z","ProcessId":124
08,"ThreadId":7540},{"ProviderId":"8983a2e6-c5d2-5a1f-691f-db243cb1f681","EventI
d":3,"Keywords":1,"Level":5,"Message":"Execute qyeru select * from dual","Opcode
":0,"Task":1,"Version":0,"Payload":{"query":"select * from dual"},"EventName":"D
BInfo","Timestamp":"2014-07-12T14:58:30.3161730Z","ProcessId":12408,"ThreadId":7
540},{"ProviderId":"8983a2e6-c5d2-5a1f-691f-db243cb1f681","EventId":2,"Keywords"
:2,"Level":4,"Message":"Process stop","Opcode":2,"Task":2,"Version":0,"Payload":
{},"EventName":"AppStop","Timestamp":"2014-07-12T14:58:30.3431778Z","ProcessId":
12408,"ThreadId":7540},

まとめ

とりあえずコンソールに出るようになりました。LogTo****メソッドでほかにファイルやAzureのテーブルやSQL Databaseなんかに出すこともできるみたいです。

コード全体

一応コード全体のせておきます。

using Microsoft.Practices.EnterpriseLibrary.SemanticLogging;
using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters;
using System.Diagnostics.Tracing;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            // リスナー作って
            var l = new ObservableEventListener();
            // 監視するログの種類を設定して
            l.EnableEvents(
                MyEventSource.Log,
                EventLevel.Verbose,
                MyEventSource.Keywords.Diagnostic | MyEventSource.Keywords.Lifecycle);
            // 何処にログを出すか決める
            l.LogToConsole(new JsonEventTextFormatter());

            MyEventSource.Log.Start();
            MyEventSource.Log.Query("select * from dual");
            MyEventSource.Log.Stop();
        }
    }

    [EventSource(Name = "MyEventSource")]
    class MyEventSource : EventSource
    {
        /// <summary>
        /// キーワード
        /// </summary>
        public static class Keywords
        {
            public const EventKeywords Diagnostic = (EventKeywords)1;
            public const EventKeywords Lifecycle = (EventKeywords)2;
        }

        /// <summary>
        /// タスク
        /// </summary>
        public static class Tasks
        {
            public const EventTask DB = (EventTask)1;
            public const EventTask App = (EventTask)2;
        }

        private MyEventSource() { }

        // シングルトンのインスタンス
        private static MyEventSource instance = new MyEventSource();

        public static MyEventSource Log { get { return instance; } }

        // 各種ログAPI
        [Event(1, Keywords = Keywords.Lifecycle, Level = EventLevel.Informational,
            Message = "Process start", Opcode = EventOpcode.Start, Task = Tasks.App)]
        public void Start()
        {
            this.WriteEvent(1);
        }

        [Event(2, Keywords = Keywords.Lifecycle, Level = EventLevel.Informational,
            Message = "Process stop", Opcode = EventOpcode.Stop, Task = Tasks.App)]
        public void Stop()
        {
            this.WriteEvent(2);
        }

        [Event(3, Keywords = Keywords.Diagnostic, Level = EventLevel.Verbose,
            Message = "Execute qyeru {0}", Opcode = EventOpcode.Info, Task = Tasks.DB)]
        public void Query(string query)
        {
            this.WriteEvent(3, query);
        }


    }
}

EnterpriseLibrary 6のException Handling Application Block

暫くストアアプリとかユニバーサルアプリとか見てたらEnterprise Libraryって6になってたんですね。

例外処理は、アプリケーションの花形!(コード量的に)なので、そこをサポートしてくれる基盤はきっちり作らないといけない。ということで、Enterprise LibraryにもException Handlling Application Blockというかたちで例外処理の部品がしっかり用意されてます。

使い方

Enterprise Library 6になって、コードでかちっと簡単に構成してインスタンス化が出来るようになったみたいですね。流れるようなインターフェースや、DIコンテナ中心の考え方はさようなら。それはそれでさみしい気はするけど、使おうと思えば使えるからいいよね。新しい書き方になれませふ。

Exception Handling Application Blockでは、IEnumerableをもとにExceptionManagerのインスタンス化が出来るようになってる。

var exManager = new ExceptionManager(new[]
{
    // ExceptionPolicyDefinitionの定義
});

ExceptionPolicyDefinitionでは、定義の名前と、どの例外をどんなふうに処理するのかを表すExceptionPolicyEntryの列挙を使って作ることができる。

ExceptionPolicyEntryは、処理する例外の型と、例外を処理した結果どうするのかというのを表すPostHandlingAction列挙体と、例外の処理を表すIExcpeitonHandlerの列挙で作ることができる。PostHandlingActionは以下の値を持ってる。

  • None: なにもしない
  • NotifyRethrow: 再度例外を投げることを通知する
  • ThrowNewException: 新しい例外を投げる

IExceptionHandlerは、例外を置き換えるReplaceHandlerや、例外をラップするWrapHandlerなんかがある。Logを処理するLogging Application Blockと連携するLoggingExceptionHandlerなんかも、用意されてたりするし、自分でIExcpetionHandlerを実装するのもあり。IExceptionHandlerは、メソッドが1つしかないので以下のように簡単に定義できる。

class MyExceptionHandler : IExceptionHandler
{
    public Exception HandleException(Exception exception, Guid handlingInstanceId)
    {
        Console.WriteLine("はんどりんぐしてログとった");
        return exception;
    }
}

長くなったけど、結局、DivideByZeroExceptionはログをとって続きの処理をやって、ExceptionはMyExceptionにラップして再度投げるという定義は以下のようになる。

var definitions = new List<ExceptionPolicyDefinition>
{
    new ExceptionPolicyDefinition("default", new[]
    {
        new ExceptionPolicyEntry(
            typeof(DivideByZeroException),
            PostHandlingAction.None,
            new[]
            {
                new MyExceptionHandler()
            }),
        new ExceptionPolicyEntry(
            typeof(Exception),
            PostHandlingAction.ThrowNewException,
            new[]
            {
                new WrapHandler("wrap", typeof(MyException))
            }),
    })
};

var exManager = new ExceptionManager(definitions);

ExceptionManagerにはProcessというメソッドが定義されていてラムダ式と、ExceptionPolicyDefinitionの名前を渡して呼び出すことができる。戻り値がある版(Func)と無い版(Action)があって、割といい感じになってると思う。

以下のような感じで使える.

// ラップしてスローしてくれる
exManager.Process(() =>
{
    Console.WriteLine("throw new Exception()");
    throw new Exception();
}, "default");

Processで例外のリスローする場合はExceptionPolicyEntryでThrowNewExceptionを指定しておかないとダメみたい。NotifyRethrowとかは、HandleExceptionという、もっと細かな制御ができるメソッドで使う。

try
{
    // 例外が発生する可能性のあるコード
}
catch (Exception ex)
{
    Exception outEx = null;
    if (exManager.HandleException(ex, "default", out outEx))
    {
        throw outEx;
    }
    else
    {
        Console.WriteLine("例外投げなくていい");
    }
}

out引数を受け取らないバージョンのHandleExceptionメソッドもあって、こっちは例外投げないといけないときは勝手に投げてくれるらしい。

他のやつも今後みていってみようかなぁ。

Enterprise Library入門 その5 「Data Access Application Block」

Data Access Application Block

ここでは、データベースにアクセスするための機能を提供するData Access Application Blockについて説明します。Data Access Application Blockを使うと、データベースにアクセスするための定型的なコードを簡略化することができます。
Data Access Application Blockを使用するには、プロジェクトを作成してNuGetからEnterpriseLibrary.Dataをインストールします。

Data Access Application Blockを利用するには、Fluent APIを利用することも出来ますがログと同様に接続先のDBは構成ファイルで管理するのが一般的だと思うのでFluent APIは利用せずに構成ファイルで接続文字列を管理するようにします。

主な提供機能

Data Access Application Blockは、Databaseと呼ばれるクラスを使って各種データベースへのアクセスを行います。1メソッドでDataSetやDataTableへSQLを使ってデータを読み込むことや、DbReaderを取得することが出来ます。また、SQLからPOCOにデータをつめこむということも行えます。その他、ストアドプロシージャのDbCommandに対するサポート機能が用意されていますが、ここではその部分についての説明は省略します。詳しくはEnterprise Libraryのドキュメントを確認してください。

データベースの作成

ここでは、SQL Server Compact Edition 4.0に簡単なテーブルを定義してSQLを使ってデータを読み書きする方法を示します。DataSetやDataTableを利用する方法は下位互換のための機能だと思うので、ここではSQLからPOCOへデータを詰め込む機能に絞って説明を行います。
コンソールアプリケーションを新規作成しターゲットフレームワーク.NET Framework 4に変更します。そして、sample.sdfという名前でSQL Server Compact Edition 4.0のデータベースを作成します。作成したデータベースに下記の構造を持ったテーブルを作成します。


ID列は、下記のようにデータベースで自動的に採番されるようにします。

データは初期状態で3件登録しました。

構成ファイルの編集

構成ファイルは、Enterprise Libraryの構成ファイルを編集するツールを使用します。app.configをプロジェクトに追加して右クリックからEdit configuration fileを選択してツールを起動します。ツールを起動したらDatabase Settingsの箇所を下図のように、先ほど作成したSQL Server Compact Edition 4.0のデータベースに接続するようにSqlCeという名前(任意の名前で問題ありません)構成します。そして、デフォルトにSqlCeを設定して保存します。

データベースへのアクセス

データベースにアクセスするにはEnterprise LibraryのコンテナからDatabaseクラスを取得します。そしてIEnumerable ExecuteSqlStringAccessor(string)メソッドを使ってSQL文を発行します。型引数のTはSQLの実行結果を格納するクラスを指定します。今回は”SELECT Id, Name, Age FROM PERSON ORDER BY Age DESC”というSQL文を実行するつもりなので、その結果を格納するプロパティを持ったPersonクラスを定義します。

01. class Person
02. {
03.     public long Id { get; set; }
04.     public string Name { get; set; }
05.     public int Age { get; set; }
06. }
このクラスにデータを格納するコードを下記に示します。
01. // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
02. var database = EnterpriseLibraryContainer.Current.GetInstance<Database>();
03. // SQL文を発行してデータを格納
04. var people = database.ExecuteSqlStringAccessor<Person>(
05.     "SELECT Id, Name, Age FROM PERSON ORDER BY Age DESC");
06. // 結果を表示
07. foreach (var p in people)
08. {
09.     Console.WriteLine("Id: {0}, Name: {1}, Age: {2}", p.Id, p.Name, p.Age);
10. }

4行目〜5行目がデータを取得している箇所になります。このコードの実行結果を以下に示します。

Id: 3, Name: ohta, Age: 30
Id: 2, Name: kimura, Age: 20
Id: 1, Name: tanaka, Age: 10

パラメータつきのSQL文を実行する方法は下記のようになります。パラメータと引数を対応づけるには通常はDbCommandのParametersにDbParameterを追加しますが、SqlStringAccessorではIDbParameterMapperの実装クラスでパラメータと引数の対応付けを行います。Enterprise Libraryでは、特別な実装は用意されていないので利用者が要件にあった実装を行う必要があります。例えば@p1, @p2, @p3…のようなルールのパラメータに引数を当てはめるIDbParameterMapperは下記のような実装になります。

01. /// <summary>
02. /// @p1, @p2, @p3という名前の順番でパラメータをマッピングするパラメータマッパー
03. /// </summary>
04. class SequenceParameterMapper : IParameterMapper
05. {
06.     /// <summary>
07.     /// デフォルトインスタンス
08.     /// </summary>
09.     public static readonly IParameterMapper Default = new SequenceParameterMapper();
10. 
11.     public void AssignParameters(DbCommand command, object[] parameterValues)
12.     {
13.         // 引数で渡された値をCommandParameterへ変換
14.         var parameters = parameterValues
15.             .Select((value, index) =>
16.             {
17.                 var p = command.CreateParameter();
18.                 p.ParameterName = "p" + (index + 1);
19.                 p.Value = value;
20.                 return p;
21.             })
22.             .ToArray();
23.         // コマンドにパラメータを追加
24.         command.Parameters.AddRange(parameters);
25.     }
26. }

AssignParametersメソッドでパラメータをDbCommandに設定しています。このクラスを使うとパラメータつきのSQL文は下記のように実行できます。

01. // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
02. var database = EnterpriseLibraryContainer.Current.GetInstance<Database>();
03. // SQL文を発行してデータを格納
04. var accessor = database.CreateSqlStringAccessor<Person>(
05.     // パラメータつきのSQL文
06.     "SELECT Id, Name, Age FROM PERSON WHERE NAME LIKE @p1 ORDER BY ID DESC",
07.     // パラメータのマッピングルール
08.     SequenceParameterMapper.Default);
09. // パラメータを指定して実行
10. var people = accessor.Execute("%mu%");
11. // 結果を表示
12. foreach (var p in people)
13. {
14.     Console.WriteLine("Id: {0}, Name: {1}, Age: {2}", p.Id, p.Name, p.Age);
15. }

CreateSqlStringAccessorが返すDataAccessor型はExecute(params object[] parameters)というメソッドを持っているので、そこに必要な数のパラメータを渡して使用します。このコードの実行結果を以下に示します。

Id: 2, Name: kimura, Age: 20

Nameにmuを含むid:2のkimuraさんだけが抽出されていることが確認できます。

データの更新と明示的なトランザクション管理

データの更新はDbCommandを使って行います。コードは以下のようになります。

01. // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
02. var database = EnterpriseLibraryContainer.Current.GetInstance<Database>();
03. // コネクションを作成
04. using (var conn = database.CreateConnection())
05. {
06.     conn.Open();
07.     // トランザクションを開始
08.     using (var tran = conn.BeginTransaction())
09.     {
10.         // 登録対象のデータ
11.         var p = new Person { Name = "hanami", Age = 100 };
12.         // コマンドをSQLから作成
13.         var command = database.GetSqlStringCommand("INSERT INTO PERSON(NAME, AGE) VALUES(@p1, @p2)");
14.         // パラメータを追加
15.         database.AddInParameter(command, "p1", DbType.String, p.Name);
16.         database.AddInParameter(command, "p2", DbType.Int32, p.Age);
17.         // トランザクションを指定してコマンドを実行
18.         var count = database.ExecuteNonQuery(command, tran);
19. 
20.         // DB側でふられたIDを取得
21.         var newId = database.ExecuteScalar(tran, CommandType.Text, "SELECT @@IDENTITY");
22.         // 登録件数と、登録時にふられたIDを表示
23.         Console.WriteLine("inserted: {0}, newId: {1}", count, newId);
24.         // コミット
25.         tran.Commit();
26.     }
27.     conn.Close();
28. }

上記のコードは単純にINSERT文を実行するだけではなく、Data Access Application Blockにおけるコネクションの明示的な管理方法とトランザクションの明示的な管理方法を示しています。このように、DBに非依存なAPIを使ってトランザクションやコネクションの管理が簡単にできるようになっています。

TransactionScope

.NET Framework 2.0から導入されたTransactionScopeを使ったトランザクション管理にもData Access Application Blockは対応しています。何も考えずに使うと分散トランザクションに簡単に昇格してしまうことから嫌う人は多い機能(SQL Server 2008 と .NET Framework 2.0 SP1で緩和されてますが)ですが、Data Access Application BlockではTransactionScopeのトランザクションが存在している間は、Databaseクラスを経由して行った同じ接続文字列のコネクションをキャッシュしておいて、2重にコネクションが開かないように管理を行います。このためSQL Server 2008以降でなくても、分散トランザクションに昇格しにくくなっています。
まず、最初に分散トランザクションに対応していないSQL Server Compact Edition 4.0でTransactionScope内で2回コネクションを開いたときの挙動を下記に示します。

01. using (var tc = new TransactionScope())
02. {
03.     // 単純にTransactionScope内で2つのコネクションを開いて閉じる
04.     var conn1 = new SqlCeConnection(ConfigurationManager.ConnectionStrings["SqlCe"].ConnectionString);
05.     conn1.Open();
06.     conn1.Close();
07.     var conn2 = new SqlCeConnection(ConfigurationManager.ConnectionStrings["SqlCe"].ConnectionString);
08.     conn2.Open();
09.     conn2.Close();
10. }

上記のコードを実行すると下記のように例外になります。

ハンドルされていない例外: System.InvalidOperationException: 接続オブジェクトをトランザクション スコープに参加させることができません。
   場所 System.Data.SqlServerCe.SqlCeConnection.Enlist(Transaction tx)
   場所 System.Data.SqlServerCe.SqlCeConnection.Open()
   場所 以下省略

Databaseクラスを使って上記のように複数コネクションを開くように見えるコードを記載してみます。

01. // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
02. using (var tc = new TransactionScope())
03. {
04.     var database1 = EnterpriseLibraryContainer.Current.GetInstance<Database>();
05.     Console.WriteLine("最初のdatabaseオブジェクト取得 HashCode: {0}", database1.GetHashCode());
06.     for (int i = 0; i < 2; i++)
07.     {
08.         // 登録対象のデータ
09.         var p = new Person { Name = "hanami", Age = 100 };
10.         // コマンドをSQLから作成
11.         var command = database1.GetSqlStringCommand("INSERT INTO PERSON(NAME, AGE) VALUES(@p1, @p2)");
12.         // パラメータを追加
13.         database1.AddInParameter(command, "p1", DbType.String, p.Name);
14.         database1.AddInParameter(command, "p2", DbType.Int32, p.Age);
15.         // トランザクションを指定してコマンドを実行
16.         var count = database1.ExecuteNonQuery(command);
17. 
18.         // DB側でふられたIDを取得
19.         var newId = database1.ExecuteScalar(CommandType.Text, "SELECT @@IDENTITY");
20.         // 登録件数と、登録時にふられたIDを表示
21.         Console.WriteLine("database1: inserted: {0}, newId: {1}", count, newId);
22.     }
23.     var database2 = EnterpriseLibraryContainer.Current.GetInstance<Database>();
24.     Console.WriteLine("2つ目のdatabaseオブジェクト取得 HashCode: {0}", database2.GetHashCode());
25.     for (int i = 0; i < 2; i++)
26.     {
27.         // 登録対象のデータ
28.         var p = new Person { Name = "sakurai", Age = 100 };
29.         // コマンドをSQLから作成
30.         var command = database2.GetSqlStringCommand("INSERT INTO PERSON(NAME, AGE) VALUES(@p1, @p2)");
31.         // パラメータを追加
32.         database2.AddInParameter(command, "p1", DbType.String, p.Name);
33.         database2.AddInParameter(command, "p2", DbType.Int32, p.Age);
34.         // トランザクションを指定してコマンドを実行
35.         var count = database1.ExecuteNonQuery(command);
36. 
37.         // DB側でふられたIDを取得
38.         var newId = database2.ExecuteScalar(CommandType.Text, "SELECT @@IDENTITY");
39.         // 登録件数と、登録時にふられたIDを表示
40.         Console.WriteLine("database2: inserted: {0}, newId: {1}", count, newId);
41.     }
42.     tc.Complete();
43. }

複数のDatabaseオブジェクトを取得してTransactionScopeの中で複数のSQL文を発行しています。ここで複数個のコネクションが開かれていたら先ほどと同じように例外になるはずです。上記のコードの実行結果を以下に示します。

最初のdatabaseオブジェクト取得 HashCode: 58204539
database1: inserted: 1, newId: 11
database1: inserted: 1, newId: 12
2つ目のdatabaseオブジェクト取得 HashCode: 54078809
database2: inserted: 1, newId: 13
database2: inserted: 1, newId: 14

ヘルパーメソッドのすゝめ

このようにData Access Application Blockを使うと、SELECT系のコードが簡潔に、そして更新系はTransactionScope内でのコネクションのキャッシュにより昇格が起こりにくくなる機能の恩恵を受けることができます。しかし、UPDATE文やINSERT文を発行するコードはお世辞にも直感的とは言い難いです。そのため、下記のようなヘルパーメソッドを作ることで完結に更新系のSQL文の発行も行えるようになります。

01. /// <summary>
02. /// Databaseクラスへの拡張メソッド
03. /// </summary>
04. static class DatabaseExtensions
05. {
06.     // IParameterMapperに指定したパラメータマッピングルールでSQL文を実行する
07.     public static int ExecuteUpdate(
08.         this Database self,
09.         string sql,
10.         IParameterMapper mapper,
11.         params object[] parameters)
12.     {
13.         using (var command = self.GetSqlStringCommand(sql))
14.         {
15.             mapper.AssignParameters(command, parameters);
16.             return self.ExecuteNonQuery(command);
17.         }
18.     }
19. 
20.     // SequenceParameterMapperのパラメータマッピングルールでSQL文を実行する
21.     public static int ExecuteUpdate(
22.         this Database self,
23.         string sql,
24.         params object[] parameters)
25.     {
26.         return self.ExecuteUpdate(sql, SequenceParameterMapper.Default, parameters);
27.     }
28. }

上記のような拡張メソッドを定義すると、INSERT文などの更新系SQLを実行するコードは下記のようになります。

01. using (var tc = new TransactionScope())
02. {
03.     // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
04.     var database = EnterpriseLibraryContainer.Current.GetInstance<Database>();
05.     // 登録対象のデータ
06.     var p = new Person { Name = "hanami", Age = 100 };
07.     // 拡張メソッドを使って登録
08.     var count = database.ExecuteUpdate(
09.         "INSERT INTO PERSON(NAME, AGE) VALUES(@p1, @p2)",
10.         p.Name, p.Age);
11.     // DB側で割り振られたIDを取得
12.     var newId = (decimal)database.ExecuteScalar(
13.         CommandType.Text, "SELECT @@IDENTITY");
14. 
15.     // 結果を表示
16.     Console.WriteLine("inserted: {0}, newId {1}", count, newId);
17.     tc.Complete();
18. }

実行結果は以下のようになります。

inserted: 1, newId 9

Fluent APIでの設定

因みに、Data Access Application BlockをFluent APIで設定すると下記のようになります。

01. var builder = new ConfigurationSourceBuilder();
02. builder.ConfigureData()
03.     // 使用する接続文字列を取得
04.     .ForDatabaseNamed("SqlCe")
05.     .AsDefault();
06. // Enterprise LibraryのコンテナからDatabaseクラスのインスタンスを取得
07. var database = EnterpriseLibraryContainer.Current.GetInstance<Database>();
08. // SQL文を発行してデータを格納
09. var people = database.ExecuteSqlStringAccessor<Person>(
10.     "SELECT Id, Name, Age FROM PERSON ORDER BY ID DESC");
11. // 結果を表示
12. foreach (var p in people)
13. {
14.     Console.WriteLine("Id: {0}, Name: {1}, Age: {2}", p.Id, p.Name, p.Age);
15. }

ForDatabaseNamedで接続文字列の名前を指定して、AsDefaultでデフォルトで使用するものに設定します。実行結果については割愛します。

Enterprise Library入門 その4 「Logging Application Blockは構成ファイルに定義しましょう」

構成ファイルによるログの設定

ConfigurationSourceBuilderによるFluent APIでの構成はプログラムでログの設定が組み立てられて慣れるととてもいいのですが、ログに関しては障害の調査のために一時的にログをErrorからInformationに下げて取得したいといったことが考えられます。このように、再コンパイルを行わずに構成変更することが考えられるログは、本番では構成ファイルによって設定したほうが良いと考えられます。
Enterprise Libraryにはグラフィカルに構成ファイルを編集できるツールがついているのでそれを使用して構成を組み立てることができます。


Logging Application Blockの構成は下記のような構造になっています。

カテゴリを定義して、それに対してログの出力先やルール(ローリング等)を定義するリスナーを複数紐づけます。さらにリスナーにログの出力形式を指定するフォーマッタを紐づけます。カテゴリには全てのイベントを受け取るカテゴリと、処理されなかったカテゴリを受け取るものと、ログ内部でエラーが起きたときのための特殊なカテゴリも定義されています。

フォーマッタで指定可能な書式

Logging Application BlockのTextFormatterでは{}で括られたプレースホルダでログのメッセージやタイムスタンプなどを任意の並びで表示するように構成可能です。代表的なものに、ログの文字列を指定する{message}や、タイムスタンプを指定する{timestamp}、ログの重要度を指定する{severity}などがあります。このフォーマットを組み立てるには、Enterprise Libraryの構成ツールのTemplate Editorで作成するのが楽なのでお勧めです。
Template Editorは、構成ツールのLog Message Fomattersの下にText Formatterを作成し、そのTemplateの横にある[…]というボタンをクリックすることで表示させることが可能です。

Enterprise Library入門 その3 「Logging Application Block」

Logging Application Block

ここではLogging Application Blockについて説明します。業務アプリケーションにおいて、ログは必須の構成要素です。地味な機能ですが、テスト時やリリース後の障害発生時の問題切り分けの重要な情報になります。また適切なログを出力して監視することで、障害の予兆を検知したり様々な利用方法があります。
.NETでは、System.Diagnostics.Traceを使ったログ出力機能が標準で備わっていますが、業務システムで必須の機能セットを備えているかと言われると力不足と言わざるを得ません。例えば、ファイルサイズが1000KBになったタイミングで今のログファイルを別名で退避するといったありがちな機能がありません。また、障害発生時に必要となるプロセスIDやアプリケーションドメイン名、コンピュータ名などの様々な情報を出力するような機能も用意されていません。
Enterprise LibraryのLogging Application Blockは、業務アプリケーション開発にひつような上記のSystem.Diagnostics.Traceに不足している機能セットを提供します。

ログ出力機能の設定

ここでは、Fluent APIで設定できる設定項目について説明します。完全な設定内容は、リファレンスの下記のページから参照してください。ここでは代表的ないくつかを実際の記述例をもとに説明します。
Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Fluent Namespace

Fluent APIの難点は、APIドキュメントを見ただけでは何が出来るのかわからないという点にあると思います。そのため、インテリセンスとサンプルを見て記述のコツをつかむのが大事です。以下に、単純にログに出力するケースの設定コード例を示します。

01. var builder = new ConfigurationSourceBuilder();
02. builder.ConfigureLogging()
03.     // 名前を付けてログの定義を開始
04.     .LogToCategoryNamed("General")
05.         // WithOptionsで追加オプション
06.         // ここではGeneralをデフォルトのカテゴリとして設定
07.         .WithOptions.SetAsDefaultCategory()
08.         // フラットファイルに出力ファイル名はdefault.log
09.         .SendTo.FlatFile("FlatFileListener").ToFile("default.log")
10.         // フィルタリング(警告以上を表示する)
11.         .Filter(SourceLevels.Warning)
12.         // ログのフォーマットを指定
13.         .FormatWith(new FormatterBuilder()
14.             // フォーマッタの名前を指定
15.             .TextFormatterNamed("LogFormatter")
16.             // フォーマットを指定
17.             .UsingTemplate("{timestamp(local:yyyy/MM/dd HH:mm:ss.fff)}:  {severity}: {message}"));
  1. ConfigureLoggingメソッド
    1. ログの構成を開始するメソッド
  2. LogToCategoryNamedメソッド
    1. 名前を付けてカテゴリを作成するメソッド。後続のメソッドチェインでカテゴリの設定を行う。再度、LogToCategoryNamedメソッドが呼ばれるまで、このメソッドで作成したカテゴリに対する設定になる。
  3. WithOptionsプロパティ
    1. ログにオプションを追加する。ここではデフォルトのカテゴリとして指定している。
  4. SendToプロパティ
    1. ログの出力先を指定する。ここではフラットファイルを指定している。このほかにもイベントログやMSMQやDatabaseなど様々な出力先に対応している。
  5. Filterメソッド
    1. ログのフィルタリングを行う条件を指定する。ここでは警告以上のログを出力するようにしている。
  6. FormatWithメソッド
    1. ログのフォーマットを指定するメソッド。フォーマットはFormatBuilderというヘルパークラスがあるので、それを使って作成する。フォーマットに指定できる名前は、LogEntryクラスのプロパティ名と大体同じ。(LogEntryクラス

上記の構成をした状態で下記のようなログを出力するコードを記載します。

01. var l = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
02. // Verbose〜Criticalまでのログを出力
03. l.Write(new LogEntry { Message = "VerboseMessage", Severity = TraceEventType.Verbose});
04. l.Write(new LogEntry { Message = "InformationMessage", Severity = TraceEventType.Information});
05. l.Write(new LogEntry { Message = "WarningMessage", Severity = TraceEventType.Warning});
06. l.Write(new LogEntry { Message = "ErrorMessage", Severity = TraceEventType.Error});
07. l.Write(new LogEntry { Message = "CriticalMessage", Severity = TraceEventType.Critical});

Enterprise LibraryのコンテナからLogWriterを取得してログを出力します。LogWriterのWriteメソッドを使用してログを出力できます。ここでは、LogEntryというログに出力する情報を表すクラスを渡すオーバーロードを指定してVerbose〜Criticalまでのログを出力しています。このプログラムを実行すると下記のログが出力されます。

----------------------------------------
2012/04/09 22:41:24.096:  Warning: WarningMessage
----------------------------------------
----------------------------------------
2012/04/09 22:41:24.110:  Error: ErrorMessage
----------------------------------------
----------------------------------------
2012/04/09 22:41:24.110:  Critical: CriticalMessage
----------------------------------------

Filterメソッドで指定したとおり警告(Warning)以上のログしか出力していないことが確認できます。

ローリングの設定

先ほどのサンプルでは単純なフラットファイルでのログの出力だったので、ここではファイルを一定の条件でローリングする設定方法について説明します。

01. var builder = new ConfigurationSourceBuilder();
02. builder.ConfigureLogging()
03.     // 名前を付けてログの定義を開始
04.     .LogToCategoryNamed("General")
05.         // WithOptionsで追加オプション
06.         // ここではGeneralをデフォルトのカテゴリとして設定
07.         .WithOptions.SetAsDefaultCategory()
08.         // フラットファイルに出力ファイル名はdefault.log
09.         .SendTo.FlatFile("FlatFileListener").ToFile("default.log")
10.         // フィルタリング(警告以上を表示する)
11.         .Filter(SourceLevels.Warning)
12.         // ログのフォーマットを指定
13.         .FormatWith(new FormatterBuilder()
14.             // フォーマッタの名前を指定
15.             .TextFormatterNamed("LogFormatter")
16.             // フォーマットを指定
17.             .UsingTemplate("{timestamp(local:yyyy/MM/dd HH:mm:ss.fff)}:  {severity}: {message}"))
18.     // Rollingという名前でログの定義を開始
19.     .LogToCategoryNamed("Rolling")
20.         // SendTo.RollingFileで一定の条件を満たしたらローリング
21.         .SendTo.RollingFile("RollingFileListener")
22.             // 1000KBでローリング
23.             .RollAfterSize(1000)
24.             // 1分間隔でローリング
25.             .RollEvery(RollInterval.Minute)
26.             // ローリングしたファイルにタイムスタンプをつける
27.             .UseTimeStampPattern("yyyyMMddHHmmssfff")
28.             // 10世代管理
29.             .CleanUpArchivedFilesWhenMoreThan(10)
30.             // ファイル名はrolling.log
31.             .ToFile("rolling.log");

上記コードの19行目からが、ローリングの設定です。SendToでRollingFileメソッドを使うことでローリングの設定が出来ます。その後に、ローリングするときの条件(サイズや時間)にローリングしたときのファイル名の命名規約や何世代までログを管理するか指定します。上記のような設定で、ログを大量に出力した結果を以下に示します。

1000KBのファイルが10世代、管理されていることが確認できます。今回のコードではローリングするログの設定をカテゴリ名”Rolling”で作成したため、ログ出力の際にこのカテゴリ名を指定する必要があります。(指定しない場合はデフォルトに設定しているGeneralが使用されます。ログ出力部分のコードは下記のようになります。

01. var l = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
02. l.Write("ログメッセージ", "Rolling");
03. l.Write(new LogEntry { Message = "sample message", Categories = { "Rolling" } });

LogWriterのWriteメソッドの第二引数でカテゴリ名を設定します。LogEntryを使用する場合はCategoriesプロパティにカテゴリを文字列の配列で渡します。Categoriesプロパティに複数のカテゴリを指定することで、一度に複数個所にログを出力することも可能です。

イベントログへの出力

最後にイベントログへの出力例を説明します。イベントログに出力するにはSendToのあとにEventLogメソッドを呼び出します。そして、UsingEventLogSourceメソッドで何処に出力するか指定します。コード例を以下に示します。

01. var builder = new ConfigurationSourceBuilder();
02. builder.ConfigureLogging()
03.     // 名前を付けてログの定義を開始
04.     .LogToCategoryNamed("General")
05.         // WithOptionsで追加オプション
06.         // ここではGeneralをデフォルトのカテゴリとして設定
07.         .WithOptions.SetAsDefaultCategory()
08.         // フラットファイルに出力ファイル名はdefault.log
09.         .SendTo.FlatFile("FlatFileListener").ToFile("default.log")
10.         // フィルタリング(警告以上を表示する)
11.         .Filter(SourceLevels.Warning)
12.         // ログのフォーマットを指定
13.         .FormatWith(new FormatterBuilder()
14.             // フォーマッタの名前を指定
15.             .TextFormatterNamed("LogFormatter")
16.             // フォーマットを指定
17.             .UsingTemplate("{timestamp(local:yyyy/MM/dd HH:mm:ss.fff)}:  {severity}: {message}"))
18.     // Rollingという名前でログの定義を開始
19.     .LogToCategoryNamed("Rolling")
20.         // SendTo.RollingFileで一定の条件を満たしたらローリング
21.         .SendTo.RollingFile("RollingFileListener")
22.             // 1000KBでローリング
23.             .RollAfterSize(1000)
24.             // 1分間隔でローリング
25.             .RollEvery(RollInterval.Minute)
26.             // ローリングしたファイルにタイムスタンプをつける
27.             .UseTimeStampPattern("yyyyMMddHHmmssfff")
28.             // 10世代管理
29.             .CleanUpArchivedFilesWhenMoreThan(10)
30.             // ファイル名はrolling.log
31.             .ToFile("rolling.log")
32.     // EventLogという名前でログの定義を開始
33.     .LogToCategoryNamed("EventLog")
34.         // EventLogに送信するEventLogListener
35.         .SendTo.EventLog("EventLogListener")
36.         // ソースはApplication
37.         .UsingEventLogSource("Application");

33行目からが追加したイベントログの定義になります。コメントにあるようにApplicationのログに出力するように設定しています。この状態で下記のようなコードを書くとイベントログにログが出力されます。

01. var l = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
02. l.Write("EventLogMessage", "EventLog", 0, 0, TraceEventType.Information);

プログラムを実行して、イベントログを確認するとログが出力されていることがわかります。

その他の機能

この他にもLogging Application BlockにはデータベースやWCFへのログの出力やメールなど一般的な用途に使えそうなログ出力の機能が提供されています。また、LogWriterクラスのIsLoggingEnabledメソッドなどを使うことで不要な時はログを出力しないといったコードも作成することが可能です。

まとめ

以上でLogging Application Blockの説明は終わりです。Logging Application Blockが、かなり多機能なログ出力の機能を持っていることが確認できたと思います。地味ながら業務システムに必須のログ出力機能を提供しています。また、構成を変更することでアプリケーションロジックのコードには手を入れることなくログの出力先やログのフィルタリングも行えます。特に採用するログ出力ライブラリが決まっていないときの選択肢の1つとして検討してみても良いと思います。