ASP.NET Coreで帳票生成サービスを作る
ここからは先に説明した帳票生成ライブラリを利用して、アプリケーションを構築する際のアーキテクチャの一例を紹介します。
まずは対象のアプリケーションの前提を確認しておきます。本稿ではExcelで設計したテンプレートから、請求書PDFを生成するサービスを構築します。せっかくExcelをテンプレートとして利用できるので、次の要件を満たすものを作ります。
- 表示する項目が変更なければレイアウトは自由に変更可能
- 対象とする帳票は固定の項目と、明細を持つ請求書(下図参照)
- 明細テーブルの項目数が多くなればページングする
- ページングした場合、テーブルの項目名は新しいページにも表示する
- Excelの式や書式を最大限に利用する
今回は先のコンソールの例と同じように、下図の一番左のExcelファイルをテンプレートとして利用し、中央と右側のようなPDF帳票を生成します。
また今回のサービスは、ASP.NET Coreで構築し、Azure上で動作させることとします。ASP.NET Coreを選定した主な理由は次の通りです。
- Windows FormsやWPFが動作する.NET Core 3.0はプレビュー段階なため
- .NET Standardの恩恵を、現時点でもっとも受けられるため
- Excelアプリケーションが動作しないサーバーサイド アプリケーションのため
採用技術
本稿のサンプルアプリケーションは以下の環境で構築しています。
- Visual Studio 2017 version 15.9.5
- Azure SQL Database
- .NET Core 2.1
- .NET Standard 2.0
- DioDocs for Excel 1.5.0.8
サンプルコードはGitHubで公開しています。併せてご覧ください。
アプリケーションの概要
アプリケーションの題材としては、Azure SQL Database用のサンプルデータベースであるAdventureWorksLTを利用します。データは英語になってしまいますが、手頃な日本語のサンプルデータベースもないので、そこは妥協します。
以下は、AdventureWorksサンプルデータベースのビジネスシナリオとして公開されている文章の抜粋です。
AdventureWorksサンプルデータベースは、Adventure Works Cyclesという架空の大規模多国籍製造企業をベースにしています。この企業は、北米、ヨーロッパ、およびアジアのマーケットを対象に、金属製自転車や複合材製自転車の製造および販売を行っています。
今回はAdventure Works Cycles社における販売情報をもとに、販売情報の一覧を表示し、その中から指定の顧客の販売情報に対する請求書をPDFで生成してダウンロードするWebアプリケーションを構築していきます。
先にも記載したように、今回はAzure上でASP.NET Coreでアプリケーションを構築します。Azure上の全体の大まかな配置モデルは次の通りです。
Azureの利用するサービスは以下の通りです。
- Azure App Service
- Azure SQL Database
- Azure Blob Storage
Blob StorageにExcelのテンプレートファイルを登録、そこから都度ダウンロードして帳票を生成します。テンプレートの管理は今回の趣旨からそれるため、今回はAzure Portalから直接Blob Storageにテンプレートファイルをアップロードして利用します。
これらが次のようにコラボレーションして、まずは請求の一覧を表示します。
ユーザーがASP.NET Coreで構築したWebアプリケーションを開くと、App Serviceに配備されたWebアプリケーションはSQL Databaseへ接続し請求書の一覧情報を取得、ユーザーのブラウザに次のように表示します。
ASP.NET Core MVCのプロジェクトテンプレートから生成した画面に、ComponentOneのFlexGridを表示しています。
ユーザーがグリッドのいずれかの請求を選択すると、Webアプリケーションは選択された請求に対する請求書を作成するのに必要な情報をSQL Databaseから取得します。そして、請求書のテンプレートとなるExcelファイルをBlob Storageから取得後、DioDocsを利用してPDFを生成しダウンロードされます。
ソフトウェア アーキテクチャ概要
下図がAzure App Serviceに展開する、ASP.NET Core MVCアプリケーションのコンポーネント図です。
薄だいだい色のコンポーネントが今回作成したものです。ピンク色のコンポーネントは既存のコンポーネントで、DioDocs含めてNuGetからインストールして利用します。
自作のコンポーネントは「InvoiceService.Web」と「DioDocs.FastReportBuilder」を除き、インターフェースパッケージと実装パッケージに分かれています。他のコンポーネントへの依存はすべてインターフェースに対して発生するように設計し、実体はDependency Injection(以降DI)パターンを利用して解決します。
DIパターンを実現するにあたり、DI ContainerとしてSimpleInjectorを利用しています。DI Containerへモジュールを登録するのは「InvoiceService.Web」で、ASP.NET Coreを起動するStarupクラスの中で実施しています。このため「InvoiceService.Web」だけは例外的に実装コンポーネントを含むすべてのコンポーネントに対して依存しています。図には線が重なり合いすぎるため記載していません。
SimpleInjectorの利用方法については以下のコードもしくはブログの記事をご覧ください。
個別のコンポーネントについて大まかに説明していきましょう。
DioDocs.FastReportBuilder
今回もっとも重要になる、帳票生成サービスを提供するためのコンポーネントです。構成要素はインターフェースのその実装クラスになりますが、インターフェースはDioDocsに一切依存しない形で定義しています。
ExcelからPDFを生成できる代替手段を、DioDocsを利用して作成した帳票生成ライブラリと同じインターフェースで用意できる保証はありませんが、UseCase.Implのユニットテストをする際に、DioDocsに依存していない方がテストの自由度が上がるためこのようにしています。
さて「InvoiceService.UseCase」と「InvoiceService.Repository」も同様ですが、利用者へ対して提供するサービスの「ルール」つまり「契約(Contract)」をインターフェースとして提供します。この「契約」が変更にならない限り、実装の詳細が変更になっても利用者側は影響を受けないように実装します。
インターフェースに対する実体の解決はDIコンテナで行うため、利用者は実装に対して一切の依存なく利用することができます。
DIパターンとDI Containerの詳細については、以下も併せてご覧ください。
本サンプルでのDIパターンの扱いについては「InvoiceService.UseCase.Impl」コンポーネントの項でクラス図とコードを見ながら解説します。
InvoiceService.Web
「InvoiceService.Web」は大きく2つの役割を実装しています。
- Presentation(View+Controller)層の実装
- DI含む依存関係と、設定項目の管理
ビジネスロジックの呼び出し結果をもとに、ユーザーとの対話を実装します。ビジネスロジックは「InvoiceService.UseCase」コンポーネントを呼び出すことによって利用します。
InvoiceService.UseCase
ユースケースつまりビジネスロジックのインターフェースと、そのインターフェースに登場するクラスを定義しています。
InvoiceService.UseCase.Impl
「InvoiceService.UseCase」の実装コンポーネント。ユースケースを実現するためのビジネスロジックを実装しています。
Implという名前空間に違和感がある人もいるかもしれませんが、UseCaseをポリモーフィズムで拡張することは現実的に考えられず、テスト用のMockを除くと正規の実装モジュールは他に発生しえないため、このような名称にしています。
ビジネスロジックを実現するにあたり、次の2つの実装については他のコンポーネントに分離しています。
- 永続化層(今回はSQL Database)から必要な情報の取得
- 請求書オブジェクトからPDF帳票の生成
これらは「InvoiceService.Repository」と「DioDocs.FastReportBuilder」を利用することで実現しています。
「InvoiceService.UseCase.Impl」コンポーネントを詳しく見ることで、コンポーネント間の結合をどのように実現しているか理解することができます。ここはもう少し詳しく掘り下げて見ていきましょう。
下図はコンポーネントに含まれる代表的なインターフェースと実装、その依存関係を表記したモデルです。
UseCaseの実装クラスが他のクラスの実装に全く依存していないことが見て取れます。私の経験上これは非常に重要なポイントです。なぜならテスト戦略に幅を持たせることができるからです。
テストを自動化していくにあたって、「単体テスト」「結合テスト」「システムテスト」のどこでどれだけテストをするのかというのは永遠の課題であると思います。すべてのフェーズで完ぺきなテストを実施することは、コストや開発期間だけでなく、保守容易性を下げる結果にもつながります。そのため最低限のコストで最高の評価を求められます。
システムの価値はユーザーにどれだけ価値を提供できるかで決定されます。したがって、価値が正しく提供できていることをより確かに確認できるテストほど価値の高いテストになります。つまりシステムテストやユーザーの受け入れテストが自動化されると、テストの価値は最大化されます。
しかしそれは何らかの課題の発見が先送りされることも意味するため、プロジェクトのリスクも増加します。
個人的にはUIのテストは統合した上で行いたいケースが多いと思っていますが、ビジネスロジックは早い段階でテストしておきたいと考えています。ここでビジネスロジック(今回はUseCase)とRepositoryやReportBuilderが疎結合になっていることの価値が発生します。
テストの価値はそれらを統合した形で実施することで最大化されます。しかし同時にRepositoryで扱うデータが大きなものであるなどして、PDFを扱う箇所をビジネスロジックも含めて一体でテストすることは困難を伴うことがあります。
PDFは内部に生成日時を持っており、生成されるバイナリが毎回異なります。そのため、自動テストの実施は困難となります。ReportBuilderの品質評価ではビジネスロジックと分離したいという要求が必ず発生するので、UseCaseとReportBuilderは疎結合になっており、テスト時にMockと差し替えが可能な設計が必要です。
Repositoryはもう少し状況が面倒です。Repositoryで取り扱うデータが小さい場合はビジネスロジックと統合してテストした方がいいですし、逆に広範なデータを扱う(多数のDBのテーブルを扱うなどの)場合は、分離してテストをした方が効率的です。
面倒なのはシステム全体でルールを統一すると、いずれかの箇所で必ず非効率なテストが発生してしまうということです。そのため統合した形でテストをするか、分離してテストをするか、選択できる余地を残しておく必要があります。
UseCaseとReporitoryの疎結合を保つ理由がここにあります。こうした話は私のブログの以下の記事でも取り扱っています。カンファレンスで登壇した内容のまとめなので、やや読みにくいかと思いますがよろしければご覧ください。
このように、特に帳票生成を他のロジックから切り離して、インターフェースと実体を分離して差し替えられるようにしておくことで、開発生産性やテスト容易性を確保することができます。繰り返しになりますが、PDFは生成時に期待結果のPDFとバイナリ比較できないため、ビジネスロジックから帳票生成のロジックは分離しておかないとテストが困難になってしまうので注意してください。
さて今回はアプリケーションのアーキテクチャが本論ではないため、このくらいにしておきましょう。詳細が気になる方は、こちらにすべてのコードを公開しているのでご覧ください。
本稿執筆時に発見した不具合について
ところで本稿を執筆している間に、期待した動作と異なる振る舞いに遭遇しました。下図の表の、外枠の罫線をよく見てください。
Excelのテンプレートは黒ですが、生成されたPDFでは一部水色になってしまっています。グレープシティ社に問い合わせたところ、DioDocs for Excelの不具合が原因で発生する問題のようです。今後、修正する項目として確定したとのことでした。
そもそもExcelやWordでは、Microsoftの製品を利用していても画面の表示と印刷に差異が発生することが起こりがちです。ミリ単位でのズレや細かな表現の差異が許容できない場合は、DioDocsではなくActiveReportsといった帳票専用製品を利用したほうがいいでしょう。
ただし、必ずしもそこまでシビアに捉えるべき問題かどうかは一考の余地があります。今回のケースでも表のデザインを修正すれば枠線を黒くすることも可能でした。細かなズレも帳票に余裕を持って設計しておけば済むことがほとんどでしょう。いずれにせよ、製品選定の際にそのあたりの評価は必要になると思います。
ただ私は、このことをPR記事である本稿にぜひ載せてほしいとおっしゃったグレープシティ社に改めて信頼感を覚えました。
不具合がない製品はありませんが、それを隠ぺいせず、向き合って対処する姿勢がある開発元とは、継続的なパートナーとして協力していきたいと考えています。
さいごに
ここまでお付き合いいただきありがとうございました。DioDocsの魅力が十分に伝わったのではないでしょうか。DioDocsは掛け値なしに素晴らしい可能性を秘めた製品だと思います。実績が伴ってくれば、単純構造の帳票の生成領域に関して、その勢力図を大幅に塗り替えるかもしれません。
非常に面白い製品ですし、NuGetに公開されているDioDocsをライセンスなし版として試すことができます。また、この記事で作成した帳票生成サービスを改良してより多くのDioDocsの機能を試したい場合は、評価ライセンスを申請すると有効期限内に限りすべての機能が利用可能になります。