Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

Storyboard のentry point をプログラムで変更する方法

概要

iOSアプリ開発において、アプリ起動時の画面を動的に切り替えたい場合がある。
例えば、以下のようなケース。

  • アプリ初回起動時はログイン画面、それ以外はログイン後の画面を出したい時。
  • デフォルトではヘルプ画面を表示して、表示しない設定を設定された場合は以降表示しないようにする。

このような動きを Storyboard を使っているときにどうすればいいのかと思ったら、簡単にできたのでメモ。

やること

プロジェクト作成して、以下の2個のファイルを変更します。

  • Main.storyboard
  • AppDelegate.swift

f:id:kouki_hoshi:20180820013418p:plain


まず、Main.storyboard に切り替えるためのviewを2つ追加します。こんな感じにしておきます。
黄色い方が初回起動時に表示する画面、緑の方が2回目の起動画面にします。
f:id:kouki_hoshi:20180820013601p:plain

プログラムから参照するために、entry point にしたいもう一つの ViewController に名前つけます。今回はStoryboard ID に secondView とつけます。
f:id:kouki_hoshi:20180820014120p:plain

ここまでで storyboard の変更は完了。

AppDelegate.swift の以下のメソッドを以下のように修正します。(logFirstLanch、lanchIsFirstTimeメソッドは下記)

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        if lanchIsFirstTime() {
            logFirstLanch()
            return true
        }
        if let window = self.window, let storyboard = window.rootViewController?.storyboard {
            window.rootViewController = storyboard.instantiateViewController(withIdentifier: "secondView")
            window.makeKeyAndVisible()
        }
        return true
    }

初期ビューを切り替えているのは下記部分。storyboard に先ほどつけた名前(secondView)から取得して、rootViewControllerに設定しています。

            window.rootViewController = storyboard.instantiateViewController(withIdentifier: "secondView")
            window.makeKeyAndVisible()

あとは、初回時と2回目以降を区別するためにユーザデフォルトに保存するための処理を書きます。

    private let STORED_KEY = "lanched"
    func logFirstLanch() {
        return UserDefaults.standard.set(true, forKey: STORED_KEY)
    }
    func lanchIsFirstTime() -> Bool {
        return !UserDefaults.standard.bool(forKey: STORED_KEY)
    }

これだけで1回目と2回目以降の初期ビューが切り替わります!

c#で値の未設定/デフォルト値を見分ける方法

概要

何も値が設定されていない時は、明示的に null を設定した時と挙動を分けたいことがある。
しかし、c# には javascript の undefine リテラルに相当するリテラルが存在せず、オブジェクトに何も設定せずに参照すると default(T) になるため何も値を設定していないのか、default(T) を設定されたのか分からず困る。

例えば以下のような、作成・更新データ形式をクラス構造で表現している時。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            // member.Age が デフォルト値(0)なら、データを 0 にする? それとも更新しない?
            // member.Name が デフォルト値(null) なら、データを null にする? それとも更新しない?
        }
    }

    public class Member {
        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

作成時には、データある/なしの場合は、値を指定する/ null で可能なのだが、
更新時には、これ以外に 「更新しない」という挙動が基本的に必要だ。そうしなければユーザの意図せず値がデフォルト値に更新されてしまうからだ。
これをどのように実現するかをメモっておく。

解決方法考察

  • 更新しないための「定数」を用意
  • Update引数をMember →Dictionary
  • 値の未設定/デフォルト値設定を分別できるオブジェクトにする
更新しないための「定数」を用意

「更新しない」という定義がなけりゃそう言った意味の定数を定義すれば良い。という考え方。
この方法はわかりやすいが危なっかしいコードになりやすい。コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (member.Age == Member.AgeNotUpdate) // Age を更新しない
            if (member.Name == Member.NameNotUpdate) // Name を更新しない
        }
    }

    public class Member {
        public static readonly int AgeNotUpdate = 13579;
        public static readonly string NameNotUpdate = "########";

        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

このコードを使う時は、下記のように使う。

        var data = new Member();
        data.Id = "123456";
        data.Age = 23;      // data.Age = 13579; の場合は変更しようとしても更新されない
        data.Name = null; //null にしたい場合
        // data.Name = Member.NameNotUpdate; //これで更新されない。ただし分かり辛い...
        new MemberManager().Update(data);

このコードの問題は、その「定数値」の更新処理ができなくなること。
もし、その値を偶然指定されてしまった場合は分かり辛いバグになること。
この方式自体が使う側にとっては直感的に分かり辛いこと。

Update引数をMember →Dictionary

そもそもオブジェクトがデフォルト値と明示的に設定されたかを区別できないことが問題なのだから、データ定義を変えればいい。という考え方。
この方法は「定数」による解決で発生する問題は解決できる。

しかし、Dictionary はデータ構造を共通にしないといけないため、型チェックができずメンテナンス性が悪くなりやすい。
コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Dictionary<Member.Key, object> member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (!member.ContainsKey(Member.AgeKey)) // Age を更新しない
            if (!member.ContainsKey(Member.NameKey)) // Name を更新しない
        }
    }
    public class Member {
        // ハッシュキー
        public static readonly Key IdKey = new Key("Id");
        public static readonly Key AgeKey = new Key("Age");
        public static readonly Key NameKey = new Key("Name");
        public class Key {
            public string Value { get; }
            private Key(string value) { Value = value; }
        }

        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

このコードを使う時は、下記のように使う。

        var data = new Dictionary<Member.Key, object>()
        {
            { Member.IdKey, "123456" }
            { Member.AgeKey, 23 }
            { Member.NameKey, "AAA" } // 更新しない時はこの記載を外す。
            //{ Member.NameKey, null } // null にしたければ null を代入する。
        }
        new MemberManager().Update(data);

「更新しない」ケース、null、値の欠けもない。完璧に見える。だが、このコードは型チェックができない。
つまり以下の書き方も可能だ。

        var data = new Dictionary<Member.Key, object>()
        {
            { Member.IdKey, "123456" }
            { Member.AgeKey, "AAA" } // 本来整数なのに文字列でも入ってしまう
            { Member.NameKey, "AAA" }
        }
        new MemberManager().Update(data);

また、データ型定義を変更した場合もコンパイルエラーが出ないためメンテナンスには苦労すると思われる。

値の未設定/デフォルト値設定を分別できるオブジェクトにする

Dictionary のいいところを利用して、型のコンパイルエラーも検知したいという仕様。オブジェクトの皮を被ったDictionary を作る。
コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            var dictionary = member.ToDictionary();
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (!dictionary.ContainsKey(Member.AgeKey)) // Age を更新しない
            if (!dictionary.ContainsKey(Member.NameKey)) // Name を更新しない
        }
    }
    public class Member {
        // ハッシュキー
        public static readonly Key IdKey = new Key("Id");
        public static readonly Key AgeKey = new Key("Age");
        public static readonly Key NameKey = new Key("Name");
        public class Key {
            public string Value { get; }
            private Key(string value) { Value = value; }
        }

        // 内部データ
        private Dictionary<Member.Key, object> _dictionary = new Dictionary<Member.Key, object>();
        public Dictionary<Member.Key, object> ToDictionary() { return _dictionary; }

        // プロパティの動作挙動定義
        private T Get<T>(Member.Key key) {
            return _dictionary.containsKey(key) ? (T)_dictionary[key] : default(T);
        }
        private void Set<T>(Member.Key key, T value) {
            if (_dictionary.containsKey(key)) _dictionary[key] = value;
            else _dictionary.Add(key, value);
        }

        // _dictionary から値を取得、値を設定
        public string Id {
            get { return Get<string>(Member.IdKey); }
            set { Set(Member.IdKey, value); }
        }
        public int Age {
            get { return Get<int>(Member.AgeKey); }
            set { Set(Member.AgeKey, value); }
        }
        public string Name {
            get { return Get<string>(Member.NameKey); }
            set { Set(Member.NameKey, value); }
        }
    }

このコードを使う時は、下記のように使う。

        var data = new Member();
        data.Id = "123456";
        data.Age = 23;
        data.Name = "AAA"; //更新しない場合は記載しない。これで_dictionary には含まれないないので更新対象にならない。
        // data.Name = null; //null にしたい場合
        new MemberManager().Update(data);

これだと型チェックもできるし、定義していない/明示的にnullを設定したかを判断できる。

C#のドキュメントコメント要素をトリムする方法

概要

XML処理のめんどくさい問題にぶち当たったのでメモ。
ドキュメントコメントからXML解析しているときに各要素の最初・最後の空白文字がうまくトリムできなかった。
今回書くのは、その解決方法。

問題事象

まず、以下のように書いたらなんか表示がおかしい。

    // XMLのマッピングオブジェクト
    [XmlRoot(ElementName="member")]
    public class Class
    {
        [XmlElement(ElementName = "summary")]
        public string Summary { get; set; }

        [XmlElement(ElementName = "typeparam")]
        public List<TypeParam> TypeParams { get; set; }

    }
    
     // TはXMLの読み込み結果をどんな形式で返すかを決定する型
     public static T Parse<T>(string documentcomment)
     {
         using (Stream stream = new MemoryStream(Encoding.Unicode.GetBytes(documentcomment.Trim())))
         {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            return (T)serializer.Deserialize(stream);
         }
     }

     // 使い方
     public static void Main(string[] args)
     {
          Class klass = XMLParser.Parse<Class>(@"
 <member>
 <summary>
 抽象クラス.
 </summary>
 <typeparam name=""T"">格納オブジェクト型</typeparam>
 </member>");

           Console.WriteLine("サマリ:" + klass.Summary);
           Console.WriteLine("型パラメータ:" + klass.TypeParams[0].Name + " (" + klass.TypeParams[0].Value + ")");
      }

すると、サマリは以下のような感じに改行や空白が入る。

サマリ:
 抽象クラス.

型パラメータ:T (格納オブジェクト型)

原因・解決法

これは、 <summary>~</summary>の間に抽象クラス.の前後に空白文字があり、XmlSerializerはそれを保持するから。
代わりに型パラメータの方は、空白が入っていないため想定通りに出ている

各要素でトリムをしたいのだ。なんとかならんのか。が、XmlSerializer にはそれっぽいメソッドはない感じ。
マッピングオブジェクト作成後に各要素をトリムするという恐ろしくスマートでないやり方も・・・。

が、調べてるといいものが見つかった。
.net - XML Deserialization of string elements with newlines in C# - Stack Overflow

You can create custom XmlTextReader class:

public class CustomXmlTextReader : XmlTextReader
{
public CustomXmlTextReader(Stream stream) : base(stream) { }

public override string ReadString()
{
return base.ReadString().Trim();
}
}

いけた!

        private class CustomXmlTextReader : XmlTextReader
        {
            public CustomXmlTextReader(Stream stream) : base(stream) { }

            public override string ReadString()
            {
                return base.ReadString().Trim();
            }
        }

        public static T Parse<T>(string documentcomment)
        {

            using (Stream stream = new MemoryStream(Encoding.Unicode.GetBytes(documentcomment.Trim())))
            {
                var reader = new CustomXmlTextReader(stream);

                XmlSerializer serializer = new XmlSerializer(typeof(T));
                return (T)serializer.Deserialize(reader);
            }
        }
サマリ:抽象クラス.
型パラメータ:T (格納オブジェクト型)

C#でのExcel操作 セル操作編

概要

前回記事より。
C#でのExcel操作取得編 - Status Code 303 - See Other

例のごとくClosed XMLを使って遊んでみる。今回は、Excelのセル操作をC#で実現する方法のメモ。
中々文献が見つからないが、触ってみるとだんだんと使いやすさが分かってくる。

テスト用のコード

今回紹介するのはセル操作のみなので、A2のcell参照を取得して、それに操作してセーブする流れとする。

	// 編集用のファイルを C:\\Users\\hoshikouki\\sample.xlsx に置いている想定。
	using (var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx"))
	{
		var ws = book.Worksheet("Sheet1");
		var cell = ws.Cell("A2");

		//
		//ここに下記コード張って楽しんでください!
		//

		// セーブ
		book.SaveAs("C:\\Users\\hoshikouki\\sample2.xlsx");
	}

セル操作

データ入力規則(リスト)
	cell.DataValidation.List(ws.Range("$E$1:$E$4"));
コメント追加
	cell.Comment.AddText("コメント!");
ハイパーリンク設定

似たようなコンストラクタのオーバーロードがいっぱいある。

	cell.Hyperlink = new XLHyperlink("Sheet2!A1");
データ形式変更
         cell.DataType = XLCellValues.Text;
         //cell.DataType = XLCellValues.Number;
         //cell.DataType = XLCellValues.DateTime;
         //cell.DataType = XLCellValues.TimeSpan;
データ表示方法変更
	cell.Style.DateFormat.Format = "yyyyMMdd";
	// cell.Style.NumberFormat.Format = "(-###)";
文字色変更
	 cell.Style.Fill.BackgroundColor = XLColor.Gold;
	 //cell.Style.Fill.BackgroundColor = XLColor.NoColor;
背景色変更
	 cell.Style.Fill.BackgroundColor = XLColor.Gold;
	 //cell.Style.Fill.BackgroundColor = XLColor.NoColor;
セル周りの線
         cell.Style.Border.BottomBorder = XLBorderStyleValues.DashDot;
         cell.Style.Border.BottomBorderColor = XLColor.Red;
         // cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thick;
         // cell.Style.Border.OutsideBorderColor = XLColor.Red;
セル内の表示場所
         // 垂直方向操作
         cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Bottom;
         // 水平方向操作
         cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right;
         // セル内に収まるように文字サイズを調整して表示
         cell.Style.Alignment.ShrinkToFit = true;
         // 折り返して全体を表示する
         cell.Style.Alignment.WrapText = true;

C#でのExcel操作取得編

概要

C#Excelを操作した調査内容。今回は、NuGetから取得できるライブラリClosedXMLを使ったメモ。

インストー

Nugetから「OpenXML」をインストール。

Excel操作インスタンス取得

ファイルを開いていると IOException が出るので、閉じてから実行すること。

	var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx");

ファイルを開くため当然閉じる処理も必要だが、IDisposableを実装しているため using 構文が使える。

	using(var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx"))
	{
		// 処理
	}

シート取得

	// ○番目のシート取得
	var sheet = book.Worksheet(1);
	// シート名で取得
	var sheet = book.Worksheet("Sheet1");	

存在しないシート名や存在しないシート数指定するとExceptionが発生する。
シート名指定の英大・小文字は特に区別しないらしく、"Sheet1"を"SHEET1"でも"SHEet1"でも取得できる。

セル取得

	// (行,列)番号で取得
	var cell = sheet.Cell(i, j);
	// 範囲文字列で取得
	var cell = sheet.Cell("A5");

行列の番号は1以上で指定する。0指定するとExceptionが発生する。

セル集合取得

セル集合に対する一括操作とかで使う。

	// 使用セル全部
	var cells = sheet.Cells();
	// 範囲文字列で取得
	var cells = sheet.Cell("A1:C11");
	// 条件でセルを取得(背景色が黄色のセル)
	var cells = sheet.Cells(x => x.Style.Fill.BackgroundColor == XLColor.Yellow);

条件でセルを取得する場合、全セルを調査しているせいかOutOfMemoryExceptionが発生する。あまりやらない方が良いかも。

セル値取得

	// 値や簡単な計算の場合の値取得
	var value = cell.Value;
	// 計算値などを取得する場合
	var cell = cell.ValueCached;

Valueプロパティで取得した場合、一部関数(ROW(), COLUMN()など)がサポートされていないようで例外が発生することがある。
ValueCachedプロパティはROW()、COLUMN()が動いた。けれど計算した値でなければNullになる。
このことから、以下のようにすればどちらでもいけそう。

	var value = cell.ValueCached ?? cell.Value;

計算式取得

	var formula = cell.FormulaA1;

なんでA1なんて名前がついているのか分からんけど、式は取得はできた。

C#のAOPライブラリ(Fody)

概要

前回記事(C#のAOPライブラリ(PostSharp) - Status Code 303 - See Other
上記とは別のライブラリ(無料版)を使ってAOPを試した内容。
GitHub - Fody/Fody: Extensible tool for weaving .net assemblies

使い方

サンプルクラス(PostSharpとほぼ同様)

全体処理は以下のようにする。

using System;

namespace FodySample
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Start Main");

			new Sample().Execute();

			Console.WriteLine("End Main");
			Console.Read();
		}
	}
}

今回は以下のSampleクラスのメソッドに対して、メソッド開始・終了にログを出力できるようにする。

using System;

namespace FodySample
{
	class Sample
	{
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		public string GetVal()
		{
			return "Fody";
		}
	}
}

上記プログラムは、以下の出力になる。

Start Main
Hello Fody!
End Main
Fodyインストー

NuGetから以下のパッケージをインストールする。

  • Fody (ver 1.29.4)
  • MethodDecorator.Fody (ver 0.9.0.6)

Fodyの安定版の最新は2.1.0(2017/6/25現在)だが、このバージョンはビルド時に以下のエラーが出て動かない。

Fody: The weaver assembly 'MethodDecorator.Fody, Version=0.9.0.4, Culture=neutral, PublicKeyToken=null' references an out of date version of Mono.Cecil.dll (cecilReference.Version). At least version 0.10 is expected. The weaver needs to add a NuGet reference to FodyCecil version 2.0.

検索したら何かしらエラーが出てきた。MonoCecilのバージョン違いが原因みたいだがどうすりゃ直るのか分からなかった。
Please fix this error · Issue #53 · Fody/MethodDecorator · GitHub
念のため、Fodyパッケージの参照するMono.Cecil.dllのバージョンを確認したが、0.10.0.0-beta6だったので問題なさそう。MethodDecorator側の問題?
・・こちらに関しては解決したら追記します。
今回は、1.29.4にダウングレードして動作を確認してます。

MethodDecorator.FodyはFodyに機能を組み込むアドインとして実装されているらしい。
そのため、Fodyにどのアドインを組み込むかを FodyWeavers.xml に定義しなければいけない。

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <MethodDecorator />
</Weavers>
AOP適用定義

AOP適用属性を定義。適用メソッドのログ出力。

using System;
using System.Reflection;

[module: FodySample.Logger]
namespace FodySample
{
	[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
	public class Logger : Attribute
	{
		private object instance;
		private MethodBase method;
		private object[] args;

		public virtual void Init(object instance, MethodBase method, object[] args)
		{
			this.instance = instance;
			this.method = method;
			this.args = args;
		}

		public void OnEntry()
		{
			Console.WriteLine("OnEntry {0}", method.Name);
		}

		public void OnExit()
		{
			Console.WriteLine("OnExit {0}", method.Name);
		}

		public void OnException(Exception exception)
		{
		}
	}
}
適用メソッドに属性付加
using System;

namespace FodySample
{
	class Sample
	{
		[Logger]
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		[Logger]
		public string GetVal()
		{
			return "Fody";
		}
	}
}

すると、AOP適用したプログラムは以下の出力になる。

Start Main
OnEntry Execute
OnEntry GetVal
OnExit GetVal
Hello Fody!
OnExit Execute
End Main

参考までに

MethodDecorator.Fodyの場合、コンパイルした後は以下のようになるらしい。
なお、以下はInterceptorAttributeをAOP適用属性を作った場合。

	public void Method(int value) {
	    InterceptorAttribute attribute = (InterceptorAttribute) Activator.CreateInstance(typeof(InterceptorAttribute));

		// in c# __methodref and __typeref don't exist, but you can create such IL
		MethodBase method = MethodBase.GetMethodFromHandle(__methodref (Sample.Method),__typeref (Sample));

		object[] args = new object[1] { (object) value };

		attribute.Init((object)this, method, args);

		attribute.OnEntry();
	    try {
	        Debug.WriteLine("Your Code");
	        attribute.OnExit();
	    }
	    catch (Exception exception) {
	        attribute.OnException(exception);
	        throw;
	    }
	}

コンソールアプリ内のSampleクラスをILSpyで逆コンパイルした結果は以下のようになった。

using System;
using System.Reflection;

namespace FodySample
{
	internal class Sample
	{
		[Logger]
		public void Execute()
		{
			MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(methodof(Sample.Execute()).MethodHandle, typeof(Sample).TypeHandle);
			Logger logger = (Logger)Activator.CreateInstance(typeof(Logger));
			object[] args = new object[0];
			logger.Init(this, methodFromHandle, args);
			logger.OnEntry();
			try
			{
				Console.WriteLine("Hello {0}!", this.GetVal());
				logger.OnExit();
			}
			catch (Exception exception)
			{
				logger.OnException(exception);
				throw;
			}
		}

		[Logger]
		public string GetVal()
		{
			MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(methodof(Sample.GetVal()).MethodHandle, typeof(Sample).TypeHandle);
			Logger logger = (Logger)Activator.CreateInstance(typeof(Logger));
			object[] args = new object[0];
			logger.Init(this, methodFromHandle, args);
			logger.OnEntry();
			string result;
			try
			{
				string text = "Fody";
				result = text;
				logger.OnExit();
			}
			catch (Exception exception)
			{
				logger.OnException(exception);
				throw;
			}
			return result;
		}
	}
}

PostSharpとの違い

OnExitの挙動が違う

PostSharpでは、AOP適用後のOnExitメソッドはfinally句にあったが、Fodyでは、try句の最後にあるため、以下のような違いになる。

  • PostSharp: AOP適用メソッドで例外が発生しようがOnExitメソッドが動く
  • Fody: AOP適用メソッドで例外が発生した場合はOnExitは実行されない

C#のAOPライブラリ(PostSharp)

AOP(Aspect Oriented Programming)?

ログ出力や例外処理など、メソッド全体に共有な処理を重複定義せず一か所に定義したいことがある。
このような共有処理を側面(Aspect)として定義した後、メソッドに適用する手法。

もし、全部それぞれにコピペなどで定義しまった場合。
その部分に修正が入った場合、全てのメソッドを直さないといけなくなり、保守性が低下する。
さらに、本来クラスにさせたい処理でないため、コードの見通しや可読性が低下する。

PostSharp

C#では有名なAOPライブラリ。本来は有料だが、Expressエディションを使えば適用数が制限されるが、無料で実行できる。
(プロジェクト単位:最大10個、ソリューション:最大50個)

なお、最上位版のPostSharp Ultimateを購入すると大体10万円かかるらしい。
PostSharp – the #1 pattern-aware extension to C# and VB

どんなとき使うの?

ドキュメントに記載されている、よくある使用法。

  • プロパティ値が変更されたときの通知
  • UndoとRedo
  • 契約プログラミング(事前条件/事後条件確認)
  • ログ・監視・プロファイリング
  • 例外発生時の制御
  • トランザクション制御
  • マルチスレッド時の挙動制御(バックグランド・フォアグランド制御、不変性・同期など)
  • セキュリティ(入力チェック、認証など)

使い方

サンプルクラス

全体処理は以下のようにする。

using System;

namespace PostSharpSample
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Start Main");

			new Sample().Execute();

			Console.WriteLine("End Main");
			Console.Read();
		}
	}
}

今回は以下のSampleクラスのメソッドに対して、メソッド開始・終了にログを出力できるようにする。

using System;

namespace PostSharpSample
{
	class Sample
	{
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		public string GetVal()
		{
			return "PostSharp";
		}
	}
}

上記プログラムは、以下の出力になる。

Start Main
Hello PostSharp!
End Main
PostSharpインストー

NuGetからPostSharpをインストールする。なお、ビルド時に設定ウィンドウが現れるので、Expressを使う。

AOP属性定義

今回は、メソッド起動時および終了時に起動ログを標準出力する属性を定義する。

using PostSharp.Aspects;
using PostSharp.Serialization;
using System;

namespace PostSharpSample
{
	[PSerializable]
	class LoggerAttribute : OnMethodBoundaryAspect
	{

		public override void OnEntry(MethodExecutionArgs args)
		{
			Console.WriteLine("OnEntry {0}", args.Method.Name);
		}

		public override void OnExit(MethodExecutionArgs args)
		{
			Console.WriteLine("OnExit {0}", args.Method.Name);
		}
	}
}
適用メソッドに属性付加
using System;

namespace PostSharpSample
{
	class Sample
	{
		[Logger]
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		[Logger]
		public string GetVal()
		{
			return "PostSharp";
		}
	}
}

すると、AOP適用したプログラムは以下の出力になる。

Start Main
OnEntry Execute
OnEntry GetVal
OnExit GetVal
Hello PostSharp!
OnExit Execute
End Main

参考までに

ドキュメントはこちら。PostSharp Documentation
今回の属性適用によって、ビルド時に適用されたメソッドは以下のようになるらしい。
参照:OnMethodBoundaryAspect Class

int MyMethod(object arg0, int arg1)
{
   OnEntry();
   try
   {
    // Original method body. 
    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

コンソールアプリ内のSampleクラスをILSpyを用いて逆コンパイルすると以下のようになっていた。

using PostSharp.Aspects;
using PostSharp.ImplementationDetails_f584e409;
using System;

namespace PostSharpSample
{
	internal class Sample
	{
		public void Execute()
		{
			MethodExecutionArgs methodExecutionArgs = new MethodExecutionArgs(null, null);
			<>z__a_1.a2.OnEntry(methodExecutionArgs);
			try
			{
				Console.WriteLine("Hello {0}!", this.GetVal());
			}
			catch (Exception exception)
			{
				methodExecutionArgs.Exception = exception;
				throw;
			}
			finally
			{
				<>z__a_1.a2.OnExit(methodExecutionArgs);
			}
		}

		public string GetVal()
		{
			MethodExecutionArgs methodExecutionArgs = new MethodExecutionArgs(null, null);
			<>z__a_1.a3.OnEntry(methodExecutionArgs);
			string result;
			try
			{
				string text = "PostSharp";
				result = text;
			}
			catch (Exception exception)
			{
				methodExecutionArgs.Exception = exception;
				throw;
			}
			finally
			{
				<>z__a_1.a3.OnExit(methodExecutionArgs);
			}
			return result;
		}
	}
}

この他にも、無料のAOPツールにFodyってのがあるらしいが、上手く動かなかったため後日再チャレンジ予定。