この記事は公開から2年以上経過しています。
.NET開発でサーバーサイドオートメーション(PIA)経由でEXCEL(Microsoft Office)操作を行う際、分離されたアプリケーションドメイン上で処理を行うことでMarshal.ReleaseComObject()
やGC.Collect()
で頑張らずにCOMオブジェクトを安全かつクリーンに扱う方法を紹介します。
今回はCOMオブジェクトを解放する目的でアプリケーションドメインの分離を行っていますが、一般的にはプラグインDLL開発などで利用できる手法です。
サンプルソースコード(C#)
VisualStudioのソリューションは、以下の3つのプロジェクトで構成します。
1. 共通インタフェース用アセンブリプロジェクト(LibExcelCommon.csproj)
分離されたアプリケーションドメイン上で実行したい処理の外部インターフェースと、当該インターフェースで利用するパラメーター用の外部インターフェースを定義します。
本例ではbool型の戻り値を返し、値オブジェクトをパラメーターにもつファンクションを1つだけ定義しています。
ILibExcel.cs
namespace LibExcelCommon
{
// Excel操作を分離されたアプリケーションドメイン上で行うクラスの外部インターフェース
public interface ILibExcel
{
void CreateWorkbook(IParameter param);
}
// ILibExcelが利用するパラメーター用の外部インターフェース
public interface IParameter
{
string FilePath { get; }
}
}
2. Excel操作ライブラリプロジェクト(LibExcel.csproj)
分離されたアプリケーションドメイン上で実行する処理を実装(前手順の外部インターフェースを実装)したクラスを作成します。
マーシャリングを行う必要があるため、MarshalByRefObject
クラスの継承が必要です。
(LibExcelCommon.csproj
をプロジェクト参照に追加、NuGetからMicrosoft.Office.Interop.Excel
パッケージを導入)
LibExcelDomain.cs
using System;
using System.IO;
using LibExcelCommon;
using Excel = Microsoft.Office.Interop.Excel;
namespace LibExcel
{
// 分離されたアプリケーションドメイン上で実行するクラス ※マーシャリングするためにMarshalByRefObjectの継承が必要
public class LibExcelDomain : MarshalByRefObject, ILibExcel
{
public bool CreateWorkbook(IParameter param)
{
try
{
// ① Excelを起動
var excel = new Excel.Application();
// ② 新規ワークブックを作成
var book = excel.Workbooks.Add();
// ③ パラメーターで指定された名称でワークブックを保存
book.SaveAs(
Path.Combine(
Directory.GetCurrentDirectory(),
param.FilePath));
excel.Quit();
return true;
}
catch (Exception e)
{
// 予期せぬエラー
return false;
}
}
}
}
3. アプリケーション本体プロジェクト(RunExcelOnNewDomain.csproj)
プログラム本体の処理を作成します。
新しいアプリケーションドメインを生成し、そのアプリケーションドメイン上でExcel操作クラスをインスタンス化して実行します。
アプリケーションドメインを跨いだパラメーターの引き渡しを行うため、パラメータークラスはシリアル化可能(Serializable
属性やISerializable
を実装)である必要があります。
(LibExcelCommon.csproj
をプロジェクト参照に追加、LibExcel.csproj
の出力先ディレクトリをRunExcelOnNewDomain.csproj
の出力先ディレクトリに設定)
Program.cs
using LibExcelCommon;
using System;
using System.IO;
namespace RunExcelOnNewDomain
{
internal class Program
{
// Excel操作クラスを実行するアプリケーションドメイン名
private const string libExcelDomainName = "LibExcel Domain";
// Excel操作クラスのアセンブリ名
private const string libExcelAssemblyName = "LibExcel";
// Excel操作クラスのクラス名
private const string libExcelClassName = "LibExcelDomain";
private static void Main(string[] args)
{
// ① Excel操作クラスを実行するアプリケーションドメインを生成
var libExcelDomain = AppDomain.CreateDomain(libExcelDomainName);
// ② 手順①のアプリケーションドメイン上でExcel操作クラスのインスタンスを生成
var domClass = libExcelDomain.CreateInstanceAndUnwrap(
libExcelAssemblyName,
$"{libExcelAssemblyName}.{libExcelClassName}"
) as ILibExcel;
// ③ 手順②のインスタンスメソッドを実行
var excelFilePath = Path.Combine(
Directory.GetCurrentDirectory(),
$"{DateTime.Now.Ticks}.xlsx");
var ret = domClass.CreateWorkbook(new Params { FilePath = excelFilePath });
if (ret)
Console.WriteLine($"Excelファイル'{excelFilePath}'を作成しました。");
else
Console.WriteLine("Excelファイルの作成でエラーが発生しました。");
// ④ 手順①のアプリケーションドメインを破棄
AppDomain.Unload(libExcelDomain);
Console.ReadKey();
}
// パラメータークラス ※値渡しするためシリアル化可能であること
[Serializable]
private class Params : IParameter
{
public string FilePath { get; set; }
}
}
}
検証
上記プログラムを実行してExcel操作ライブラリ内でブレークすると、下図のように分離されたアプリケーションドメイン上でのみExcelが読み込まれていることが確認できます。
今回はExcelをクリーンに扱う方法を紹介しましたが、特にプラグイン開発の場合はセキュリティ向上やプログラムの結合度を低下させる(≒単体テストしやすくなる)観点などからも、アプリケーションドメインを分離する方法をおすすめします。
以上です。