kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

カテゴリーアーカイブ: ASP.NET

.NET Core 1.1 で msbuild はどうなったのか


一度は nuget統合がらみで project.json とかに移行するという形でdiscon言われた msbuild ですが、結局息を吹き返し、Visual Studio 2017 RC で .NET Core App (ASP.NET vNext 改め ASP.NET Core でのWebアプリを含む)のビルドランナーとして復活を果たしました。

この msbuild について、ビルドのカスタム手法について諸々調べてみましたので blog にまとめてみます。

デフォルトの csproj からの targets / props の読み込み

実際にプロジェクトを作って csproj を開いてもらうと解りますが、msbuild の Import による targets の取り込みがありません。過去の知識だけを頼りに調べようとすると一体何が起こってるのやらで即詰みしますね。

いつ増えたんだ!って感じですが、 msbuild に /pp:出力ファイル名 で import によるファイルインクルードの解決結果を出力するオプションがあるのでこれを使って標準ターゲットを取り込んだ XML を生成すると、その冒頭で回答が出てました。

<?xml version=”1.0″ encoding=”utf-8″?>
<!–
==========<snip>===============================================
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\WebApplication2.csproj
==========<snip>===============================================
–>
<Project ToolsVersion=”15.0″ DefaultTargets=”Build”>
  <!–
==========<snip>===============================================
  <Import Project=”Sdk.props” Sdk=”Microsoft.NET.Sdk.Web”>
  This import was added implicitly because of the Project element’s Sdk attribute specified “Microsoft.NET.Sdk.Web”.

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

==========<snip>===============================================
–>

csproj ファイルの冒頭、Sdk アトリビュートによって、Sdk.props の読み込みが行われていて、Sdk.props は C:\Program Files\dotnet\sdk\… にあるという事でした。

実際の csproj での Project 要素のSdkアトリビュートは以下のようになっています。

<Project ToolsVersion=”15.0″ Sdk=”Microsoft.NET.Sdk.Web”>

この Sdk 属性により関連 props が読み取られます。

この Sdk.props は以下のように終わります。

  <!–

==========<snip>===============================================

  </Import>

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

==========<snip>===============================================

==========<snip>===============================================
  </Import>

C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\WebApplication2.csproj
==========<snip>===============================================
–>

続いて csproj での定義内容が現れ、

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <PropertyGroup>
    <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
  </PropertyGroup>
  <PropertyGroup>
    <UserSecretsId>aspnet-WebApplication2-898cd849-2e88-466a-81b4-e1d20363bffb</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include=”Microsoft.ApplicationInsights.AspNetCore” Version=”2.0.0-beta1″ />
    <PackageReference Include=”Microsoft.AspNetCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Authentication.Cookies” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Identity.EntityFrameworkCore” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.AspNetCore.Mvc” Version=”1.1.1″ />
    <PackageReference Include=”Microsoft.AspNetCore.StaticFiles” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.Design” Version=”1.1.0″ PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.SqlServer” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.SqlServer.Design” Version=”1.1.0″ PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.EntityFrameworkCore.Tools” Version=”1.1.0-msbuild3-final” PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.Extensions.Configuration.UserSecrets” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.Extensions.Logging.Debug” Version=”1.1.0″ />
    <PackageReference Include=”Microsoft.VisualStudio.Web.CodeGeneration.Design” Version=”1.1.0-msbuild3-final” PrivateAssets=”All” />
    <PackageReference Include=”Microsoft.VisualStudio.Web.BrowserLink” Version=”1.1.0″ />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include=”Microsoft.EntityFrameworkCore.Tools.DotNet” Version=”1.0.0-msbuild3-final” />
    <DotNetCliToolReference Include=”Microsoft.Extensions.SecretManager.Tools” Version=”1.0.0-msbuild3-final” />
    <DotNetCliToolReference Include=”Microsoft.VisualStudio.Web.CodeGeneration.Tools” Version=”1.0.0-msbuild3-final” />
  </ItemGroup>

csproj の Project 要素内が展開された続きが以下です。

  <!–
==========<snip>===============================================
  <Import Project=”Sdk.targets” Sdk=”Microsoft.NET.Sdk.Web”>
  This import was added implicitly because of the Project element’s Sdk attribute specified “Microsoft.NET.Sdk.Web”.

C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets
==========<snip>===============================================
–>
  <!–
***********************************************************************************************
Sdk.targets

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
–>

csproj の Project 要素の末尾に Sdk.targets が暗黙に展開されるようになっています、これが Sdk アトリビュートの仕組みという事ですね。 Project 要素に Sdk アトリビュートを付けると先頭に props 、末尾にtargets が挿入される、そういう仕組みです。

SDK として配布展開される props / targets の参照方法としては非常にシンプルで良いと思います。

csproj.user はどうなった

プリプロセス結果から Import Project を拾ってみた結果は以下です。

長いですけど、結果的に csproj.user の読み込みは無くなってます。

途中赤字にしてるのがプロジェクト配下の props / targets の読み込みでビルドのカスタム化という点で言えば有望株です。

<Import Project=”Sdk.props” Sdk=”Microsoft.NET.Sdk.Web”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.props

<Import Project=”Microsoft.NET.Sdk.DefaultItems.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.DefaultItems.props

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.CSharp.props” Condition=”‘$(MSBuildProjectExtension)’ == ‘.csproj'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.CSharp.props

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.props

<Import Project=”$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props” Condition=”‘$(ImportProjectExtensionProps)’ == ‘true’ and exists(‘$(MSBuildProjectExtensionsPath)’)”>
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\WebApplication2.csproj.nuget.g.props

<Import Project=”$(NuGetPackageRoot)microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props” Condition=”Exists(‘$(NuGetPackageRoot)microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props’)”>
C:\Users\kazuk\.nuget\packages\microsoft.diasymreader.native\1.4.0\build\Microsoft.DiaSymReader.Native.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.props

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\Sdk\Sdk.props

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\Microsoft.NET.Sdk.Publish.props

<Import Project=”Sdk.targets” Sdk=”Microsoft.NET.Sdk.Web”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.BeforeCommon.targets” Condition=”‘$(IsCrossTargetingBuild)’ != ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.BeforeCommon.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.DefaultOutputPaths.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.DefaultOutputPaths.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.TargetFrameworkInference.targets” Condition=”‘$(TargetFramework)’ != ””>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.TargetFrameworkInference.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.RuntimeIdentifierInference.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.RuntimeIdentifierInference.targets

<Import Project=”$(LanguageTargets)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.CSharp.targets

<Import Project=”$(CSharpTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.CSharp.CurrentVersion.targets

<Import Project=”$(CSharpCoreTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Roslyn\Microsoft.CSharp.Core.targets

<Import Project=”Microsoft.Common.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.Common.targets

<Import Project=”$(CommonTargetsPath)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.Common.CurrentVersion.targets

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter\*” Condition=”‘$(ImportByWildcardAfterMicrosoftCommonTargets)’ == ‘true’ and exists(‘$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.targets\ImportAfter\Microsoft.NuGet.ImportAfter.targets

<Import Project=”$(NuGetRestoreTargets)” Condition=”Exists(‘$(NuGetRestoreTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\NuGet.targets

<Import Project=”$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter\*” Condition=”‘$(ImportByWildcardAfterMicrosoftCommonTargets)’ == ‘true’ and exists(‘$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\15.0\Microsoft.Common.targets\ImportAfter\Microsoft.TestPlatform.ImportAfter.targets

<Import Project=”$(VSTestTargets)” Condition=”Exists(‘$(VSTestTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Microsoft.TestPlatform.targets

<Import Project=”$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets” Condition=”‘$(ImportProjectExtensionTargets)’ == ‘true’ and exists(‘$(MSBuildProjectExtensionsPath)’)”>
C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\WebApplication2.csproj.nuget.g.targets

<Import Project=”$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets” Condition=”Exists(‘$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets’)”>
C:\Users\kazuk\.nuget\packages\microsoft.extensions.configuration.usersecrets\1.1.0\build\netstandard1.0\Microsoft.Extensions.Configuration.UserSecrets.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\Microsoft.NET.Sdk.targets” Condition=”‘$(IsCrossTargetingBuild)’ != ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.targets

<Import Project=”Microsoft.NET.Sdk.Common.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.Common.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.PackageDependencyResolution.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)Microsoft.PackageDependencyResolution.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.PackageDependencyResolution.targets

<Import Project=”Microsoft.NET.Sdk.DefaultItems.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.DefaultItems.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.DisableStandardFrameworkResolution.targets” Condition=”‘$(DisableStandardFrameworkResolution)’ == ‘true'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.DisableStandardFrameworkResolution.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.GenerateAssemblyInfo.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.GenerateAssemblyInfo.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Publish.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Publish.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.PreserveCompilationContext.targets”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.PreserveCompilationContext.targets

<Import Project=”$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.CSharp.targets” Condition=”‘$(Language)’ == ‘C#'”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.CSharp.targets

<Import Project=”$(NuGetBuildTasksPackTargets)” Condition=”Exists(‘$(NuGetBuildTasksPackTargets)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\NuGet.Build.Tasks.Pack\build\NuGet.Build.Tasks.Pack.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Web.ProjectSystem\build\netstandard1.0\Microsoft.NET.Sdk.Web.ProjectSystem.targets

<Import Project=”$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets” Condition=”Exists(‘$(MSBuildSdksPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets

<Import Project=”$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets” Condition=”Exists(‘$(MSBuildThisFileDirectory)..\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\Microsoft.NET.Sdk.Publish.targets

<Import Project=”$(WebPublishProfileFile)” Condition=”Exists(‘$(WebPublishProfileFile)’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\PublishProfiles\DefaultProfile.pubxml

<Import Project=”$(_ComputeTargetsDir)Microsoft.NET.Sdk.Publish.ComputeFiles.targets” Condition=”Exists(‘$(_ComputeTargetsDir)Microsoft.NET.Sdk.Publish.ComputeFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\ComputeTargets\Microsoft.NET.Sdk.Publish.ComputeFiles.targets

<Import Project=”$(_CopyTargetsDir)Microsoft.NET.Sdk.Publish.CopyFiles.targets” Condition=”Exists(‘$(_CopyTargetsDir)Microsoft.NET.Sdk.Publish.CopyFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\CopyTargets\Microsoft.NET.Sdk.Publish.CopyFiles.targets

<Import Project=”$(_TransformTargetsDir)Microsoft.NET.Sdk.Publish.TransformFiles.targets” Condition=”Exists(‘$(_TransformTargetsDir)Microsoft.NET.Sdk.Publish.TransformFiles.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\TransformTargets\Microsoft.NET.Sdk.Publish.TransformFiles.targets

<Import Project=”$(_PublishTargetsDir)Microsoft.NET.Sdk.Publish.$(PublishProtocol).targets” Condition=”Exists(‘$(_PublishTargetsDir)Microsoft.NET.Sdk.Publish.$(PublishProtocol).targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\PublishTargets\Microsoft.NET.Sdk.Publish.FileSystem.targets

<Import Project=”$(_DotNetCLIToolTargetsDir)Microsoft.NET.Sdk.DotNetCLITool.targets” Condition=”Exists(‘$(_DotNetCLIToolTargetsDir)Microsoft.NET.Sdk.DotNetCLITool.targets’)”>
C:\Program Files\dotnet\sdk\1.0.0-rc3-004530\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\DotNetCLIToolTargets\Microsoft.NET.Sdk.DotNetCLITool.targets

さて、有望株と言える二つに注目しましょう。

$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props と $(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets を読み込んでいます。

dotnet build /v:diag

での出力から、MSBuildProjectExtensionsPath は Project の obj 配下となります。

MSBuildProjectExtensionsPath = C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\obj\

よりによって obj かよ!って事でこれは残念。NuGet から展開された props / targets を実行するのに使ってるみたいです。

ですが、それの近くに Condition がマッチせずに Import されなかった

<Import Project=”$(DirectoryBuildPropsPath)” Condition=”‘$(ImportDirectoryBuildProps)’ == ‘true’ and exists(‘$(DirectoryBuildPropsPath)’)”>

<Import Project=”$(DirectoryBuildTargetsPath)” Condition=”‘$(ImportDirectoryBuildTargets)’ == ‘true’ and exists(‘$(DirectoryBuildTargetsPath)’)” />

な Import があります。

DirectoryBuildPropsPath はプロジェクトディレクトリの Directory.Build.props、DirectoryBuildTargetsPathはプロジェクトディレクトリの Directory.Build.targets があれば読み込むようになっています。

やってみた

Directory.build.targets をプロジェクト配下に作成します。

<?xml version=”1.0″ encoding=”utf-8″ ?>
<Project>
  <Target Name=”mySampleTarget” AfterTargets=”Build” >
    <Message Text=”mySampleTarget running” Importance=”High”/>
  </Target>
</Project>

dotnet build でビルド実行した結果、定義したターゲットが実行されました。

C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2> dotnet build
Microsoft (R) Build Engine version 15.1.523.56541
Copyright (C) Microsoft Corporation. All rights reserved.   WebApplication2 -> C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2\bin\Debug\netcoreapp1.1\WebApplication2.dll   mySampleTarget running C:\Users\kazuk\Documents\Visual Studio 2017\Projects\WebApplication4\WebApplication2>

Visual Studio でビルドしてもいけるよね?

いけるみたいです。上記でのメッセージはビルドの出力ウインドウに出ました。

まとめ

遠回りはした気もしないでもないが、 Directory.build.props / Directory.build.targets をプロジェクトディレクトリに置くだけで msbuild でのビルドプロセスはカスタマイズできます。

その昔のビルドカスタマイズポイントだった csproj.user とかはどうやら無くなってます。

なんかカスタマイズとかしたのに読まれてなくない?とか一体どこのファイルがコレやってるのとかその辺を調べるのには /pp でプリプロセス結果吐かせると便利ですね。(このファイル、Import の解決した結果を完全に吐いてくれるので別環境でビルドするには便利かもしれません。)

あとは /v:diag してログ眺めれば不思議な事なんてない(震え声

Doc of code 販売開始のお知らせ


 

滅多にやらない宣伝記事が増えまして申し訳ありませぬ。

というわけで Doc<Code> については最後の宣伝記事になるでしょう。

販売サイトがオープンしましたので、「買ってくれ!」以上のこの記事には意味はございません。

http://kazuk.azurewebsites.net/

 

んで、今月中はキャンペーンを設定します。

日本時間での今月中にライセンスを購入いただくと、ライセンスの付与期間が倍でライセンスキーが発行されます。

1年ライセンスを買っていただくと 2年後に無効になるライセンスキーが送られるという事になります。

販売初期限定という事で、どうかよろしくお願いいたします。

 

会社等での購入をお考えの方は kazuk.dll@kazuk.jp にご連絡ください。見積書、納品書による対応のほか、銀行振り込みでの入金その他は現状メールでの対応とさせて頂きます。

ASP.NET メンバーシップ、ロールを使って初期スーパーユーザーが無くて困る


タイトルのような事って起こりませんか?

管理者ロールみたいなの作ったとして、その管理者ロールに最初のユーザーをどうやって入れるねんとか。

IISマネージャがつながるとか、ASP.NETの構成ツールがつながれば、そっちからいかようにもできるんですが、そうもいかない環境ってありますよね。

簡単に言えば ASP.NET を Azure Web Sites で動かしてる時とか。

最初のユーザーを管理者ロールに入れるとかやるとして、ソレってできた瞬間から表に出てる Azure Web Sites でやって安全なん?とかとか。

アプリのつながるDBにユーザーレコード直接作ればいいっちゃ良いんだけど、IP固定してる回線でない場合には、現在IP調べてSQL Azure に一時的にそのIPを許可入れて、レコード作って、ここでIP許可外し忘れると同一プロバイダの誰かが偶然にもつながる穴が残るよねと。

まぁ、諸々面倒なんですわ、ロールベースで管理者ロール作って、しっかり管理系作ったとして最初のスーパーユーザーが居ないって事が。

そうですよね?

そんなわけで作った

要するにDBベースのASP.NET標準のメンバーシップやロールだけだと、初期ユーザーの作成とかがややこしいので、展開されるファイルベースの認証プロバイダがあればサイトが展開された時にはそのファイルに記述されたユーザーは居る事になると。

これをDBベースの認証プロバイダとカスケードしてスーパーユーザー以外はそっちにユーザー登録したり、認証するようにすればいいよねと。

PM> Install-Package ConfigBasedSecurityProviders

やってる事は単純に ExtendedMembershipProvider と RoleProvider の実装ですが、基本的にすべての更新系は基本となるプロバイダに転送し、同様に知らん事の参照系も基本となるプロバイダに流します。

そのうえでXMLで指定された多少の知っている事だけを認証処理したり認定します。

使い方

Nuget からパッケージをインストールすると、bin に認証プロバイダ本体が入ります。そして必要な web.config に対する修正が適用されます。App_Data 配下にサンプルとしてのxml が二つ入ります。あとルートに readme.html が入ります。

web.config の改行とかフォーマッティングが崩れるのは Nuget の仕業っぽいので気になるならオートフォーマットでも適用してください。

App_Data のXMLを修正して、ユーザーとパスワードを設定し、ロールのメンバーを設定します。

設定した XML は admin_users.xml / admin_roles.xml という名前に直せば、デフォルトで入る web.config の修正にあった形になります。逆に web.config 側でのファイルの位置指定を直せば、どこにでもファイルの置場や名前を設定できます。

これでファイルベースの認証を通した上で、必要な構成作業をやったうえで、要らなくなったら NuGet パッケージをアンインストールで取り外してしまってもいいし web.config の要らない所をコメントアウトして無効にしてしまっても良いでしょう。再デプロイでつぶしの反映を忘れずに。

まとめ

ASP.NET アプリケーションをクラウドに入れるのにあんまり安全じゃない事したくないよね。

認証回りの所に派手に作りこみしてバグ残すとセキュリティバグはすごく痛い思いするから初期構成の為に変な事もできないし、つけ外しがちゃんとできる格好でやらないと怖くてアレ。

そういったニーズに答えるパッケージのつもりでございます。

要するに、薄いゴムのアレですよ。うん。

注意事項

状態持たないので、ユーザーのパスワードミスによるロックアウトとかできません。なので強度の低いパスワードでユーザー作っておきっぱなしにすると、プルートフォースでやられる危険があります。

うん百文字のランダムとかプルートフォースされても「何千年頑張るの?」って言えるようにしておくか、初期構成作業を終えたら外すのお勧めです。

Doc of Code のコードから:プロバイダパターンの実装インフラ


Doc of Code では ASP.NET のプロバイダパターンを全面的に使って、モジュール間の依存性を解決しています。

というわけで、プロバイダパターンを実装する上での内部インフラストラクチャがあったりしまして、これの軽い紹介です。

プロバイダパターンとは

ASP.NET / ADO.NET その他で色々と使われているアプリケーション内でのモジュール差し込みメカニズムと言って良いでしょうかね、APIを定義するモジュールと、実装モジュールを分ける事ができ、実装は普通に web.config とかで指定する事ができます。

素の ASP.NET 上で実装する場合には ProviderBase 派生クラスを作ります。必要であれば Initialize をオーバライドし、初期化パラメーターを受け取ります。そして ProviderSettings を保持する構成セクションを作り、 ProviderCollection を初期化てプロバイダをインスタンス化します。

そのまま普通に実装するなら、以下の様なコードになります。

public abstract class HogeProvider : ProviderBase
{
    private static Lazy<ProviderCollection> _providers = new Lazy<ProviderCollection>( InitProviderCollection );
    public static ProviderCollection Providers
    {
        get { return _providers.Value; }
    }
    private static ProviderCollection InitProviderCollection()
    {
        var collection = new ProviderCollection();
        ProvidersHelper.InstantiateProviders(
            ConfigSettings.Providers,
            collection,
            typeof(HogeProvider));
        return collection;
    }
    private static Lazy<HogeSettings> _settings = new Lazy<HogeSettings>( InitSettings );
    public HogeSettings ConfigSettings
    {
        return _settings.Value;
    }
    private static HogeSettings InitSettings()
    {
        return (HogeSettings) ConfigurationManager.GetSection(“hogeProviders”);
    }
    // API’s goes here
}

public class HogeSettings : ConfigurationSection
{
    [ConfigurationProperty(“providers”, Options = ConfigurationPropertyOptions.IsRequired)]
    public ProviderSettingsCollection Providers
    {
        get { return (ProviderSettingsCollection)this[“providers”]; }
    }
    // Settings properties goes here
}

Lazy の initializer をメソッドバインドして遅延初期化とかまぁ普通にやればこうなるかなぐらいの実装ですね。

実際には defaultProvider プロパティの追加とか、それを元に Provider プロパティを出してとかやるともうちょっと伸びます。

んで、一個実装するぐらいならなんてことはない話なんですが、5つも6つも実装するとなるとぶっちゃけてお約束コードが長くて鬱になるという類です。

Initialize のオーバーライド

プロバイダ作ったら大抵なんかしら初期化のオーバーライドが必要になります。

このオーバライドをすると configs として渡される NameValueCollection をグチグチといじる必要があります。

これがコードでやると結構うっとおしいのです。

_configParam=configs[“parameterName”]==null ? defaultValue : int.Parse( configs[“parameterName”] );

をべたべた並べてやると int.Parse が失敗すれば例外吹っ飛んでいき、ダメならエラーになってくれるし、設定しくってる奴が悪いといえばそうなんだけど、ちゃんと何の設定違ってるとか例外の詳細カスタムしようとすると try {} catch { throw … } で括ってを並べる事になります。

世の中データバインディングにアノテーションだろって言ってもまぁアレです。

Doc<Code> のプロバイダ実装インフラを流用する

https://kazuktnd.wordpress.com/docofcode/ でライセンスに明記されてる通りで、「それ以外のソースコードおよびバイナリは自由に実行、解析、改変、再配布、他の製品への流用等ができます。 」の「他の製品への流用等ができます。」の通りです。アセンブリ一個取りだして流用も許諾されていますので、好きにしてください。って事です。

bin に展開されている CustomProviderInfrastructure.dll を取り出し、参照設定します。

using CustomProviderInfrastructure; // NameValueCollection の Bind 拡張メソッドに必要

public class HogeProvider: CustomProviderBase<HogeProvider, HogeSettings >
{
    [InitializeParameterRequired]
    private int _timeout;

    public override void Initialize(string name, NameValueCollection config)
    {
        config.Bind( this, self=>self._timeout, int.Parse );
    }
}
[DefaultConfigurationSectionName(“hogeSettings”)]
public class HogeSettings : CustomProviderSettingsBase
{
}

InitializeParameterRequired で _timeout が修飾されていますので、この場合 timeout は省略できません。

config.Bind は式木のプロパティ名/フィールド名から取得する要素名を決定します。デフォルトで先頭の _ は削られ、結果は camelCaseName にされます。(先頭が大文字なら小文字になる)、フィールドないしパラメータに InitializeParameterName 属性でパラメータ名が指定されれば、その名前で NameValueCollection の要素を取りだします。

変換時に例外が出たりすると、ConfigurationErrorsException にパラメータ名等付いた例外を再送出するので、例外を綺麗にするとかの為にコードを特に足す必要はありません。

Bind には省略可能パラメータでデフォルト値が指定できますので、InitializeParameterRequiredAttribute を付けない場合にはデフォルト値も指定してください。

以上で前述のコードでの ProviderCollection の絡みや ConfigurationSection の派生もすべて済みます。

web.config には DefaultConfigurationSectionName で指定されるセクションを記述します。

<hogeSettings defaultProvider=”default” >
    <providers>
        <add name=”default” type=”HogeProvider, HogeProviderServices” />
    </providers>
</hogeSettings>

あと、web.config の configSections にセクションの記述が要りますがそんなもん当然すぎるので割愛。

まとめ

好きに使って良いよなアセンブリも入ってますので、遊んであげてください。

Doc<Code> というツールをリリースします


技術特化 blog なんで、滅多にやらない事なのですが、宣伝させてください。

宣伝かよ

はい、宣伝です。軽く失注続きで本気にこのままだと路頭に迷う事態なので一人ならどうという事は無いのですが嫁子供居る身なので必死なわけで、個人で作って使ってるツールの類に値段を付けて並べてみようかとの第1弾です。

タダで手に入る物で全部回して主義の方はこの先読んでも意味ないのでお帰り下さい。

Doc<Code> “doc of code” とは

.NET Framework アセンブリとその XML ドキュメントファイルを投げ込むとドキュメントを閲覧できる ASP.NET アプリケーションです。

同様なXMLドキュメントコメントの整形を行うアプリケーションとしては Sandcastle があります。Sandcastle プロジェクト自体は現状 Sandcastle Help File Builder でメンテナンスされているようですが、若干プロジェクトに不安定要素があり、ちょっとややこしい事になっているようです。

Sandcastle との違いは、あちらはビルドプロセスでのバッチ処理でHTML、およびヘルプファイルを作成する枠組みですが、Doc<Code> では、ASP.NET アプリケーションとして要求に応じてアセンブリを随時解析してXMLドキュメントと結合表示する仕組みですので、簡単に言えばビルド時に XMLドキュメントファイルの生成以上の追加処理を必要としません。結果的にビルド時間に対するインパクトを殆ど0にする事ができます。

要するにスロービルドの原因である、誰も見ないようなドキュメント生成を、オンライン解析変換処理にする事で開発スループットをより伸ばせるツールという事です。

インストールに必要な物は?インストールの手順は?

IIS 7.5 以降(Express でも可) 、 .NET Framework 4.5が必要です。 VS2012で普通に開発してる環境なら入っている物ですね。

DocOfCode の紹介とインストールデモ等

普通に NuGet からパッケージ入れるだけで入ります。

どういう使い方が想定されていますか?

普通に開発現場でドキュメントを見る用途、および、オープンソースな物を作ってる人がAPIリファレンスをインターネット上に公開するとかに使って良い様に作っています。

インターネット公開サイトに使う場合に、アセンブリを勝手にアップロードされないようにしたい等は、AssemblyController とその関連Viewそのものを外してしまって下さい。(この場合、アップロードはファイルを直接 App_Data に配置する事でもできますし、アセンブリの一覧管理はプロバイダになっていますので、プロバイダを実装する事で管理方法そのものを変える事ができます)

表示のデザイン等に関わる View および、それに当たる css 等はすべてパッケージ内にありますので、NuGet パッケージを取りこんだうえで修正してもらえれば幾らでもカスタマイズできると思います。

使い方

単純に動かすと能書きの書かれた Index ページの上部にアセンブリと設定のリンクがありますので、アセンブリを選んでもらうと、アセンブリをアップロードするためのページへ遷移します。

アップロード時に指定するタイトルはそのままアセンブリの情報ページのパスとなります、アセンブリ内の名前空間や型、メンバ情報はすべてその配下でアクセスされます。

アップロード済みのファイルと同じタイトルでアップロードを行うと、アセンブリファイルおよび、ドキュメントコメントのXMLファイルの入れ替えになります。

後は飛び回って好きにドキュメントをみて下さいという事です。 Code Contracts で XML ドキュメントコメントを出すようにしておけばそれなりに契約内容も表示されます。

名前空間に関するドキュメントコメント

ドキュメンテーションコメントのXML仕様的には N: で名前空間についてのドキュメントを置けることになっているため、このドキュメントタグが付いた要素があれば表示されるようになっています。また、 NDoc / sandcastle と同様に NamspaceDoc という型に対するドキュメントがあれば、名前空間のドキュメントとして表示されるようにしています。

ドキュメンテーションコメント内のタグ

推奨されるドキュメンテーションコメントタグの殆どはレンダリングできるようになっています。

また、各種HTMLタグはそのままHTMLタグとして表示するようになっていますので、HTMLの aタグをドキュメントコメントに記述しておけばリンクになりますし、imgタグを記述しておけばイメージも表示されます。(当然に img はどっか Web サーバにホストしないと見れませんと思いますけど)

高度な使い方とスケーラビリティ等

Doc<Code> は解析対象アセンブリとドキュメントをすべてメモリ上に読み込んで処理を行う為、スケーラビリティのネックは基本的にはメモリ容量という事になります。また、閲覧されるドキュメントをできるだけ高速にレンダリングしたいため、メモリに読み込んだモジュールやドキュメントをできる限りメモリ保持しようとしますのでアセンブリを登録すれば登録したほどメモリを使います。開発中のドキュメント参照支援等で .NET Framework のすべてのアセンブリとドキュメントを閲覧できるサーバを立てるとかはメモリがきついとかが根本的なネックになるでしょう。

結果的に別々のサーバに、このアセンブリはこっちのサーバ、このアセンブリはこっちのサーバといった形で配置する事になります。これらのサーバ間でドキュメントのリンクを通す事がサーバリンク機能で実現できます。

App_Data/LinkedServers.txt ファイルにリンクされるサーバ名:ポート番号を記述しておくと、該当サーバに存在しないアセンブリへのドキュメントのリンクが自動で解決されます。たとえば A サーバに mscorlib を配置して、Bサーバに自前のライブラリのドキュメントを配置し、BサーバのLinkedServers.txt にAサーバを記述しておくと、Bサーバでドキュメントをレンダリングする場合に String 等 mscorlib 由来の型は A サーバへのリンクとしてレンダリングされます。

Doc<Code>サーバ間は Web API で保持しているアセンブリの問い合わせを行っていますので、Aサーバへのアセンブリの追加などを行った時にBサーバ側は特に何もしないでも自動で反映されます。

注:サーバリンクを使ってサーバ間のリンクをする場合、それぞれのサーバにライセンスが必要です。

解析系の独自拡張の実装

Microsoft CCI に関する知識は必須ですが、アセンブリ、名前空間、型、メンバのそれぞれについて、追加で解析処理を行うロジックを入れるフックポイントが用意されています。

これにより、メンバメソッドのサイクロマチック複雑度を計算するロジックを追加すると、メンバの情報取得時にそれが呼び出され、ロジックが出力した結果は View まで引き渡しされます。

今後の開発ロードマップ

現状では各種メタデータおよび、ドキュメントコメントの表示側に注力している形になっていますが、将来的にはドキュメントコメントの入力、編集ができるようにする事を計画しています。

というのも、綺麗に見えるようになれば欲が出るので、使用例とかの Example をドキュメントコメントに色々書きたくなったりするのですが、それをやるにはコメントの中にコードを書かなければならないし、コメントの中に書かれたコードの中で XML 的な制約で < や > をそのまま書けないし色々と痛々しい思いをしなければなりません。

ドキュメントを充実させたいという思いと、ソースをそんなに汚したくないという思いや色々な葛藤の結果としてドキュメントコメントを実際として機能的に捨てているという現場も多いでしょう。

その辺りに対して、ドキュメントとして入れたい物をソース外からドキュメントコメントXMLに出力できる様にするビルド時ツールとか色々絡めて、十分なドキュメントを書いてもコードファイルが汚れない、それを素早く見れて修正を入れたりとかを Web 上のツールとコード上の双方でできる様にするのを次のマイルストーンとして考えています。

お値段は?

インストールされるサーバにつき1か月300円をめどに設定しています。

販売サイトの方を準備中だったりしますが、 1ヵ月300円、3ヵ月900円、6ヵ月1600円、1年で3000円の4通りのラインナップの予定です。

販売方法としては PayPal でのクレジットカード決済で購入するとメールでライセンスファイルが送られてくるという形式、ライセンスファイルはサーバへ配置が必要です。

また、直接 kazuk.dll@kazuk.jp の方にメール頂ければ見積書、納品書等、会社関係で必要な物は出せる形ができます。

んでいつリリースなのよ

とりあえず、NuGet パッケージについては今月中には出します。販売サイトは月明け早々にも立ち上がり予定、実際のライセンス販売開始は来月10日からを予定しています。(このあたり、多少のずれはあるかもしれません)

NuGet でのパッケージングの注意事項諸々


NuGet で pack する時に失敗しそうな事を色々と見つけたので blog

web.config.transform が指定した通りには入らないっぽい

現象の発生する NuGet.exe

C:\Users\kazuk\Documents\DocOfCode\DocOfCode>..\.nuget\NuGet.exe
NuGet Version: 2.5.40416.9020

ASP.NET プロジェクトのディレクトリで nuget spec 叩いて nuspec をちょっと編集してパッケージを作れるようになりましたって状態で、web.config に関する変換を web.config.transform として作成

nuget pack を叩く

Successfully created package ‘C:\Users\kazuk\Documents\DocOfCode\DocOfCode\DocOfCode.1.0.0.33115.nupkg’.

んでNuget package explorer で中身見てみると自分が指定した web.config.transform じゃなくて、web.config の内容が入ってる。 Exclude で web.config 指定してもダメっぽい。

image

Nuget package explorer で狙った格好の web.config.transform を入れなおさないと現状だめっぽい。

web.config の中にセキュリティ的にヤバい事書いてると nuget 経由で漏れそうな気がするので注意、超注意

NuGet pack –IncludeReferencedProjects を不用意に使うと危険

なんでか知らんけど、依存プロジェクトに含まれる出力ディレクトリにコピーのついてないコンテンツまで nuget パッケージに入れられちゃう。

nuget pack –IncludeReferencedProjects を使ってパッケージ

Successfully created package ‘C:\Users\kazuk\Documents\DocOfCode\DocOfCode\DocOfCode.1.0.0.33115.nupkg’.

CreateRasKey.txt は依存プロジェクトのコンテンツなんだけど、パッケージに入っちゃう。

image

クラスライブラリプロジェクトの中に txt で作り中のメモとか置いて出力ディレクトリにコピーしてないから大丈夫だと思ってるとダダ漏れになる。(RSA秘密鍵とかそういうのをうっかりコンテンツで置いてて漏れそうになった罠)

ビルドオプションをちゃんと None にしないとダメよと。

まとめ

便利な物だけど、ファイルが表に出ていく物である以上は慎重に使ってあげないと、セキュリティ的にヤバいので、中身はちゃんと確認しましょう。

500 – Internal Server Error – One ASP.NET Advent Calender


というわけで、二日続けての Advent Calender、今回は One ASP.NET です。

というわけで、タイトルの事についてゴニョゴニョ語ってみようと思います。タイトルからして嫌なものを題材にしてって感じですが、クリスマスに向けてホノボノし続けてるとたまにホラー映画とか見たくなるじゃないですか、そういうニーズにこたえる記事でございます。

ASP.NET での Web サイトやアプリケーションを皆さんはどのように監視したりしていますか?

  1. ping や一定期間ごとの get リクエストでの死活監視
    これは最低限の監視ですね、サーバやインフラが生きてないんじゃお話にならないでしょう。
  2. 定期的なWebアクセスログやシステムイベントログの収集と分析
    システムイベントログを取ってるとアプリケーションのエラーとかの情報はおおよそ良い感じに集まるので、これをベースに監視や管理を行うのはいいことですね。Web アクセスログはうまく使えば応答時間の変動等も監視できますね。
  3. そもそもやって無いとか
    レンタルサーバにおいていてレンタルサーバ屋さん任せ、自分は特に監視や管理をしないよ!これもありっちゃありな話です。
  4. ASP.NET の監視のAPI使ってるぜ!
    これは珍しいんじゃないかな?っていう事で今回の記事になります。

System.Web.Management 名前空間

ASP.NETなら当然に参照されている System.Web アセンブリに System.Web.Management 名前空間があります。この名前空間で用意されているものが今回の題材です。MSDN でのリファレンスはこちら

Internal Server Error !

さて、 Internal Server Error が発生したら何が起こるでしょう。 Page Error イベントハンドラが起動されたり Application オブジェクトの Error メソッドが呼び出されるとかはまぁご存じのことでしょう。

このエラーの発生時には様々な事をやりたいと思うでしょう、担当を叩き起こしたり、お客への詫び状を書いたり、再発の防止策をまとめたりはさておき、DBサーバは生きてるのか確認したりとか、いろんな確認をしてどこがどんな原因で落ちたのか「ほらバグだ、さあ直せ」「ほらバグだ、さあ直せ」「ほらバグだ、さあ直せ」と踊り狂う営業をどうやって棺桶に入れるかとか色々考えることや調べること、諸々があります。

さて、まっとうに商売しているWeb屋さんなら複数のアプリケーションをもっていたり、複数の顧客のサイトを運営していたりする事はよくある事でGlobal.asax.cs なんてふつうは顧客向けカスタムの巣窟だったりしますよね。

でも、監視管理は自分達の監視管理の方針に揃えたいですよね。

顧客カスタムの巣窟なGlobal.asax には会社として揃えたい物は入れたくない、そうその通り。かといって Application_Error でしか捕まえられないじゃない!

困った結果として HttpApplication と MvcApplication の間に挟まれる中間クラスを書いたりしようにもこの辺は各アプリケーションが自分たちの実装の為に入れ替えてるのがふつうだったりしてアプリケーションの用意するHttpApplication 実装の下には入りづらいけど上に入れるのは基底クラス違いでいくつも書かないとアレとか色々な問題が起こりすぎて泣けてくるのがふつうだったりするでしょう。

能書きはおいといて実装編

さて、Application_Error でない場所で Internal Server Error をトラップしてみましょう。

会社としての管理ポリシーを実装するためのクラスライブラリを作成します。クラスライブラリでは System.Web と System.Configuration を参照設定し、 WebEventProvider の派生クラスを作ります。

image

using System.Web.Management;

namespace EnterpriseManagementPolicy
{
    public class EnterpriseWebEventProvider : WebEventProvider
    {
        public override void ProcessEvent(WebBaseEvent raisedEvent)
        {
            if (System.Diagnostics.Debugger.IsAttached)
            {
                System.Diagnostics.Debugger.Break();
            }
        }

        public override void Shutdown()
        {
        }

        public override void Flush()
        {
        }
    }
}

Webアプリケーションからクラスライブラリを参照し、web.config の system.web 配下に以下を記述します。

  <system.web>
    <healthMonitoring>
      <providers>
        <add name="Enterprise" type="EnterpriseManagementPolicy.EnterpriseWebEventProvider"/>
      </providers>
      <rules>
        <add name="RouteErrorToEnterprise" eventName="All Errors" provider="Enterprise"/>
      </rules>
    </healthMonitoring>

単純に Home の Index で ApplicationException を throw してみましょう。

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "ASP.NET MVC アプリケーションを簡単に始めるには、このテンプレートを変更してください。";

            throw new ApplicationException("落ちてみるよ!");

            return View();
        }

WebEventProvider の実装コードではデバッガがアタッチしていればブレークするようにしているので、Visual Studio からデバッグ実行するとブレークポイント処理に飛び込みます。

imageimage

やったね!

クイックウォッチで raisedEvent の中身を表示させたのが以下、

image

WebRequestErrorEvent に as キャストしてしまえば例外そのものや、それにまつわるスタックトレース、RequestInformation 配下にはアクセスされたURLまで入っているし、発生日時等一通りの情報が含まれていますのが見て取れますね。

この実装では web.config の修正や参照設定は行いましたがアプリケーションの実装には一切の変更はありませんので、顧客ニーズに応じた処理な global.asax には一切の変更は要らないですし、GACに管理ポリシーを実装するアセンブリを入れて Machine.config/ApplicationHost.config で定義してしまえば実際には web.config の修正も要りません。

http://500error.local/ みたいなローカルイントラネットアプリに HTTP-POST で必要な情報を送り込めば既知バグか判断してくれたり、既知バグだった場合にプライオリティを上げたり、既知バグでなかったら TFS にバグを起票してくれたり、例外の種類がDBサーバ死んでる的なものは DB サーバのチェックコマンドを投げてくれたり色々と自動化のネタが思いつきますね。

既存のWebEventProvider

エラーをメール通知したりとか、一般的な事を何にも書かなかったのはワザとで、エラーのメール通知は標準で存在する WebEventProvider 派生に実装済みです。

メールの送信は以下

SimpleMailWebEventProvider

TemplatedMailWebEventProvider

SQL Server にイベントを書き込むのは以下

SqlWebEventProvider

SQL Server にイベントを書きこむためには aspnet_regsql コマンドで用意されたデータベーステーブルが必要です。

実際に SQL Server にWebEventを書きこむのはどうかと思っていたりしています。(DBがあっぷあっぷしてる時のエラーをDBに書けるかという難問を解けるかですね)

OS のイベントログに書き込むのは以下

EventLogWebEventProvider

エラー時等にOSのイベントログが書かれるのは実際にはこれが動いているからですね。

WMIイベントの発生もできます。

WmiWebEventProvider

そしてIISのトレースインフラに書き込むのが以下です。

IisTraceWebEventProvider

開発中等で見るIISの Failed Request Trace に出ているものはこれが中継しています。

そして、System.Diagnostics.Trace に渡すのが以下

TraceWebEventProvider

ASP.NET のトレースを有効にしていれば、ASP.NET の黄色いアレにイベントが出る事になります。(Trace.axd にも)

カスタムすればどこにでも何とでもなりますけど、標準実装でできる事の範囲内では標準使いましょう。

(Azureでインスタンス飛ぶと遅延転送されるログが消えるのがまずいって人は重要なものを選定してストレージに流せばいいんじゃないかな?)

カスタムイベント

さて、WebEventProvider はイベントをどっかに持っていく物だったりしますが、WebEvent そのものもカスタム定義をする事ができます。

アプリケーションでカスタムイベントを使う事はアプリケーションの要件によりますが、たとえばECサイトとかを作っている場合、お金と商品のやり取りに関するデータだけは絶対に失いたくない物だったりしますよね。こういう物をカスタムイベントを使って WebEvent を流して、 SqlWebEventProvider とカスタムななんかしらのストレージに流す仕組みを作ると、それだけで絶対に失いたくない物のロギングが二重化されます。そのログ2通りからDB状態の整合を取るツールを作っておけば、何か起こった時の命綱として安心感をもたらしてくれる事でしょう。

逆に商品の在庫切れのようなものをWebEventを流しておけば、標準のメール通知のWebEventProviderで管理者に通知する事や、カスタムのWebEventProviderによって商品に応じた担当者に向けた通知を発行するなんて事も可能な形になります。えてしてこういう便利機能をアプリケーションに積み込んでいくことが実際の顧客向けのオンライン性能の制約になったりしますのでWebEventの発行とWebEventProviderでのイベント処理でステージが分離される事のメリットは結構あったりします。

んで、カスタムな WebEvent の実装方法です。 WebBaseEvent を派生するクラスを作って必要なメソッドを実装するだけです。リファレンスの最後に実装例も載ってますね。

イベントの発行は該当クラスを new して Raise を呼ぶだけです、カスタム WebEvent の発行には信頼レベルが Midium が必要な点だけは注意が必要です。

F5アタックに対する考慮

カスタムなWebEventProvider 、WebEventを実装する場合や既存のWebEventを何らかのWebEventProviderに接続する前にはF5アタックのような性能飽和攻撃(サービス拒否攻撃)に対して考慮の必要があるでしょう。未認証ユーザーからのリクエスト等を無駄に詳細なロギングをしようとするとあっという間に性能が飽和してアプリケーションの目的とする動作が実行できなくなったりします。認証ユーザーに対してでも同時ユーザー数が十分に多い場合等はアプリケーションの目的サービスの妨げとならないように性能に気を配った実装をする必要があるかもしれません。

まとめ

アプリケーションの Error 処理で Global.asax.cs で Application_Error を書くのはあんまりお勧めできないです。

ロギングしたいなら既存の WebEventProvider でできますので web.config でルール設定すればいいんです。それ以外のカスタムエラー画面出すようにするとかも web.config での設定の範囲でできる事だったりします。それ以上のエラー処理ってあんまり無かったりしませんですか?ログの体裁をむにゃむにゃしたいとかならカスタムなWebEventProvider 作ればいいじゃないかとかとか、log4net に出したいとかも、log4net に出す為の WebEventProvider 作ればよろし。失って困る物は WebEvent 発行して複数の WebEventProviderでログすればまず困らない。

とかいう私も数年前まで Application_Error になんかつじつま合わせを書いていた気がします、無知識だったゆえって感じですので今後は Application_Error を書くことは殆ど無いんじゃないかなって感じです。

Application_Error にエラー処理を書いたら負けかなと思っているとまでは言わないですけど、いろんなつじつま合わせなエラー処理が必要なアプリケーションは設計おかしくないか?という観点で見直してみて頂きたく。

この記事に書いたことのほとんどは、MSDNリファレンスの以下から参照できますので、エラー処理とか壮大なものを設計する前に読んだ方がよさそうだと思った人はどうぞ。

ASP.NET Health Monitoring の概要

では、ハッピーなクリスマスを!

ASP.NET IHttpModule での HttpApplication イベントのハンドリングに割り込む(1)


元ネタはすでに修正済みだったって事でNo.1が切腹してましたが、こんな方法もあるよというアプローチとその周辺にまつわる ASP.NET Core の闇についてちょっと書いてみます。

まず、ASP.NET において IHttpModule が HttpApplication のどのイベントをハンドルしているのか調べる方法はあるのか、イベントハンドリングに割り込む前にここから話を進めましょう。

回答としては存在します。 HttpApplication.Events プロパティ このプロパティの説明を見る通りで、EventHandlerList を舐めればイベントハンドラを列挙する事が出来ます。

「EventHandlerList を舐めれば」が可能であればこれだけで話は済みますが、そんなに簡単には行かんのです。

まず、EventHandlerList は object をキーとしてDelegateが取得できますが、キーとして使う object がどっからも取れんという事、少なくとも普通の手段ではこれを取る事はできません。リフレクションを使わない限りには。

どうせリフレクションを使うなら致死量以上の毒はいくら飲んでも同じ事なので EventHandlerList を徹底的に暴いておくのも良いでしょう。というわけで、以下の拡張メソッドが出来上がります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Reflection;
using System.Diagnostics.Contracts;

namespace Kazuk.Dark
{
    /// <summary>
    /// EventHandlerList相手のリフレクションを隠ぺいします
    /// </summary>
    public static class EventHandlerListExtention
    {
        private const BindingFlags NonPublicInstance =
            BindingFlags.NonPublic | BindingFlags.Instance;

        private static readonly Lazy<FieldInfo> _headField = 
            new Lazy<FieldInfo>(
                () => typeof(EventHandlerList)
                      .GetField("head", NonPublicInstance));

        private static bool _initialized;
        private static FieldInfo _keyField;
        private static FieldInfo _handlerField;
        private static FieldInfo _nextField;

        /// <summary>
        /// EventHandlerList の内容をDictionaryに変換します
        /// </summary>
        /// <param name="eventHandlerList"></param>
        /// <returns></returns>
        public static Dictionary<object, Delegate> 
            ToDictionary(this EventHandlerList eventHandlerList)
        {
            Contract.Requires<ArgumentNullException>(eventHandlerList!=null);

            var item = _headField.Value.GetValue(eventHandlerList);
            if (item==null) return new Dictionary<object, Delegate>();
            if (!_initialized)
            {
                Type tEntry = item.GetType();
                _keyField = tEntry.GetField("key", NonPublicInstance);
                _handlerField = tEntry.GetField("handler", NonPublicInstance);
                _nextField = tEntry.GetField("next", NonPublicInstance);
                _initialized = true; 
            }
            var result = new Dictionary<object, Delegate>();
            while (item !=null)
            {
                result.Add(
                    _keyField.GetValue(item),
                    (Delegate)_handlerField.GetValue(item));
                item = _nextField.GetValue(item);
            }
            return result;
        }
    }
}

やってる事がリフレクションという決しておすすめできないテクニックなので説得力ないかもですが、こういう事は一か所に集中させてあまり散らさないほうが良いという事でこういう拡張メソッドで集中させるのが良いという訳です。

これによって HttpApplication.Events に格納されたイベントハンドラにアクセスする事が簡単にできる様になりました。 それと同時に EventHandlerList のRemoveHandler / AddHandler を使えばイベントハンドラの差し替えが出来そうに見えます。

結果的にはこのアプローチはうまく行かないのですが、とりあえずコード化してみましょう。それがみなさんを ASP.NET Core の闇に招待する方法だからです。

やろうとする事を単純にする為に HttpApplication.Init をオーバライドして BeginRequest をハンドルします。これを EventHandlerList から検索し RemoveHandler / AddHandler で差し替えますが検索ロジックを作り上げるのも面倒なのでデバッガへトレースメッセージを送りつけるだけで何もしないロジックを間に挟み込む事にします。(何しろ比較しようにも困る object と、 Delegate が出てくるわけで上手くいかない事にそこまでやってられません。)

public override void Init()
{
    base.Init();

    BeginRequest += OnMvcApplicationBeginRequest;

    var eventDic = Events.ToDictionary();
    foreach (var ev in eventDic)
    {
        object key = ev.Key;
        Delegate handler = ev.Value;
        Events.RemoveHandler(key, handler);
        Events.AddHandler(key, WrapHandler((EventHandler) handler));
    }
}

private Delegate WrapHandler(EventHandler eventHandler)
{
    EventHandler wrapedHandler = (s, e) =>
    {
        Debug.WriteLine("Begin event");
        try
        {
            eventHandler(s, e);
        }
        finally
        {
            Debug.WriteLine("End event");
        }
    };
    return wrapedHandler;
}

void OnMvcApplicationBeginRequest(object sender, EventArgs e)
{
    Debug.WriteLine("BeginRequest");
}

これを動かした結果のデバッガ出力は以下。

image

見事に BeginRequest しか出ません。

なぜかというと ASP.NET Core は EventHandlerList を使ったイベントの発行をすでにやってないから、内部的にもっと高速にイベントを発行できる仕組みを用意しているからです。結果として EventHandlerList を操作した所で知った事なし。

実際にどうなってるのかは ASP.NET Core ソースを見てもらうのが一番ですが、 BeginRequest の event の add では HttpApplication のinternal メソッド AddSyncEventHookup によってSyncEventExecutionStepが構築され、PipelineModuleStepContainer に保持されます。

要するに見た目として .NET Framework の event / delegate によって処理が制御されている様に見えて全く異なるメカニズムで処理が実行されているわけです。

理由としては単純にパフォーマンスでしょう。Delegate の呼び出し等はdelegate の差し先が変わる事等を色々考慮すると JIT での最適化について色々と制約が生まれてしまいます、個々のイベントハンドラの呼び出しでは微々たるロスかもしれませんがアプリケーション全体としては非常に大きなパフォーマンスペナルティを生み出すわけでこれを避ける為に ASP.NET Core チームはそれなりに仕事しています。

ここまでのコードが全然無駄に見えるかもしれませんが、ちゃんと使いますのでもう少し先に進みましょう。

EventHandlerList によるイベント格納を操作する事で割り込む事は出来ない事は確かですが、 ハンドラを正規のaddメソッドを使ってちゃんと add してあげれば良いのですからこれをする方法を考えましょう。

単純に言えば EventHandlerList から取得した EventHandler を add するのは簡単です+= で渡すだけですから。同様にイベントハンドラの解除も簡単ですね –= でremove を呼べば良いわけです。

難点としてはさっき避けて通ったEventHandlerList のキー object を結局取るしかないよねって所、そんなもんは単純にリフレクションで取ればいいんだからいいやですね。

それ以外の問題としては一つは remove して add するとイベントの発行順が変わってしまうという事、これも全部を remove add する事で順序を維持すれば良いですね。

で、 Async EventHandlers が問題として残ります。例えば AddOnBeginRequestAsync 等は EventHandlerList には入らないし、 remove 方法がありません。

EventHandlerList に入らないって事はどうやって取得するの?には決まってんだろ、いわせんな恥ずかしい。って事にしてください。

結果として remove 方法が無いのをどうするって事だけが課題として残ります。

ここで HttpApplication が sealed でなく単純にnew可能なクラスである為、Fake に登録させるという方法があります。

長ったらしくなりそうなので、回を分けるという事にしました。Fake にイベントハンドラ登録させて Fake を舐めてイベントハンドラに割り込むのは次回という事で、ばいばーい、また来週、またみてねー!?(来週には書くという予告?)

ASP.NET MVC プロジェクトのサブシステム分割とマージ


なんとなく出来ちゃったという物のおすそわけ blog.

 

何を解決する物ですか?

ある意味 ASP.NET MVC による大規模アプリケーション開発をサポートします。

大規模というのは、サブシステム分割が必要となるレベルであり、例えば Controller が数十からそれ以上に達する可能性の高いシステムです。

このような大規模アプリケーションを作る場合、ASP.NET MVC の Area 等を利用してアプリケーション内で分割するのは一つのアプリケーションの規模の増大を招くだけであり、規模の暴力に対してのアプローチとはなりえません。

根本的にはアプリケーションをサブシステムに分割し、各個サブシステムを統合するという手順が望ましいわけです。

単純に msbuild プロジェクトファイル一個でこの分割によって分かれたアプリケーションの統合が出来ましたよって事です。

ソースと動作原理

Build.Utilities\HugeWebApplication.targets

<?xml version="1.0" encoding="utf-8" ?>
<Project 
  ToolsVersion="4.0" 
  DefaultTargets="Build" 
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <_copyContentSourceContent Include="@(Content)" Exclude="**/*.config" />
    <_copyContentsOutputFiles
      Include="@(_copyContentSourceContent->'$(CopyContentOutputPath)\%(identity)')" />
  </ItemGroup>

  <Target Name="CopyProjectContents">
    <Message Text="$(MSBuildProjectName) からコンテンツをコピーします 出力先 $(CopyContentOutputPath)" />
    <Copy SourceFiles="@(_copyContentSourceContent)"
          DestinationFiles="@(_copyContentsOutputFiles)" SkipUnchangedFiles="true" />
  </Target>

  <Target Name="MargeProjectContents" AfterTargets="BeforeBuild" >
    <MSBuild 
      Targets="CopyProjectContents" 
      Projects="%(ProjectReference.Identity)" 
      Properties="CopyContentOutputPath=$(MSBuildProjectDirectory)" />
  </Target>
</Project>

動作原理としては MargeProjectContents ターゲットが BeforeBuildの前に実行され ProjectReference で参照しているプロジェクトに対して CopyProjectContents ターゲットを実行します。

CopyProjectContents ターゲットは呼び出されたプロジェクト上で定義された Content を呼び出し元プロジェクトのディレクトリに Copy します。

結果として空のASP.NETプロジェクトで MVC プロジェクトをプロジェクト参照すれば MVC プロジェクトの Contents は空のASP.NETプロジェクトに転送されます。bin に MVC プロジェクトの出力が配置されるのはプロジェクト参照のデフォルトの挙動で、そこに対しては何も変更していませんので bin にMVCアプリケーションのアセンブリは配置されます。

使い方

ソリューション上に ASP.NET MVC アプリケーションを複数作成し、統合先となる空の ASP.NET アプリケーションを作成します。統合先プロジェクトから各 ASP.NET MVC アプリケーションをプロジェクト参照します。

各 ASP.NET MVC アプリケーション、および 統合先 ASP.NET アプリケーションの csproj の末尾に以下の一行を追加します。

  <Import Project="..\Build.Utilities\HugeWebApplication.targets" />

後はビルドするだけです。

image image

スクリーンショットではWebApplication1 が MvcApplication1 / MvcApplication4 を参照しています。

ビルド結果では WebApplication1 の配下に Views その他の Content がコピーされている事が解ります。

コピー対象としてかち合う事が見え見えの web.config 等 config 類は含ませていません。

Global.asax/.cs どうするかなーとか解決すべき課題が無いわけではありませんが、ここまでの労力が msbuild のtargetsファイル 20行ってのは上々の戦果といっても悪くないかなーと。

統合先として今回は空のASP.NET アプリケーションで紹介しましたが、ASP.NET MVC アプリケーションでも構わない=依存する基盤部の取り込みを各部でする為に応用も出来るという事も備考まで。

やってみたらコレ困るわな事があればコメントにどーぞです。

という訳で、ハッピーなクリスマスを!

MVC と Code Contracts と DataAnnotations


MVC でリポジトリパターン?サービスインターフェース?を Code Contracts で堅くいこうとすると、こんな恰好になりますよね?

[ContractClass( typeof( RepositoryContracts ) )]
public interface IRepository
{
    Data GetByName( string name );
}

[ContractClassFor( typeof( IRepository ) )]
abstract class RepositoryContracts : IRepository
{
    public Data GetByName( string name )
    {
        Contract.Require( name!=null );
    }
}

これをやればリポジトリを無茶なパラメータで呼ぶような事は契約によって守られるわけで素晴らしいんだけど。

namespace Application.Models
{
    public class QueryViewModel
    {
        [Require]
        public string Name { get; set; }
    }
}

なViewModel を元にコントローラで検索するこんなコード

[HttpPost]
public ActionResult Query( QueryViewModel model )
{
    if( ModelState.IsValid )
    {
        var data = _repository.GetByName( model.Name );
        return View( data );
    }
}

IsValid 確認したって、code contracts にとってはそんなこと知ったことなくで unproven 祭り。

この unproven を解消する為に Assume をここに書き込むのは避けたい、Model 側の属性で制御してる事柄を Controller に書き込んでいるも同然で、Model を仕様変更した途端に Assume は嘘に変わる可能性が無いわけではないし、そもそも DataAnnotations でやっている検証で保障してる内容を再確認するコードだし二重記述な気がして嫌だというわけ。

Model にこんなメソッドを書く

public void EnsuresAssumeAsValid( )
{
    Contract.Ensures( Name!=null );
    Contract.Assume( Name!=null );
}

そうすると、さっきのコントローラーでは model.EnsuresAssumeAsValid() と ModelState.IsValid の確認後に呼んであげれば Name が null じゃないという認識になる。

んで DataAnnotations での検証結果が反映されるようにEnsures/Assume するメソッドを書く、DataAnnotations での設定に従ってという仕事が残る。

形を変えての繰り返し作業ってメンドイよね、飽きやすいし、けどミスは許されなくて、属性書き換えたらちゃんと直さないと Code Contracts が絵に描いた餅になる。

でだ、そこで T4 だ

リフレクションで舐めるから Models は別アセンブリになる必要がある、まぁ、MVC でアプリケーション作ったら Models は別アセンブリにするのはレイヤ分離上も結構良くやる事なんで別に構わんだろう。

image

という感じで ASP.NET MVC3 のデフォルトの Models にちゃんと Contract を意識したコードを吐いてるのが以下のT4コード、これで ModelState.IsValid だったら model.EnsureAssumeAsValid() ってやれば DataContracts に従った Ensures/Assumeがされる。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.ComponentModel.DataAnnotations" #>
<#@ assembly name="$(SolutionDir)\MvcApplication2.Models\bin\Debug\MvcApplication2.Models.dll" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    // ここで属性に対する Ensures / Assume を定義します
    AttributeTypeMap.Add(
        typeof( System.ComponentModel.DataAnnotations.RequiredAttribute ),
        new EnsuresAssumeActions { 
            Ensures= (attrData, propInfo) => {#>
            Contract.Ensures( item.<#=propInfo.Name#> != null );
<#},
            Assume= (attrData, propInfo) => {#>
            Contract.Assume( item.<#=propInfo.Name#> != null );
<#}
        }
    );
#>
using System.Diagnostics.Contracts;

namespace MvcApplication2
{
    public static class ContractHelper
    {
<# 
// モデル型をちゃんと抽出する様に直してね

var typeQuery = from asm in AppDomain.CurrentDomain.GetAssemblies() where asm.GetName().Name=="MvcApplication2.Models" from t in asm.GetTypes() select t; foreach( Type t in typeQuery ) {#> // <#=t.Name#> public static void EnsureAssumeAsValid( this <#=ClassName(t)#> item ) { <# foreach( var p in t.GetProperties() ) { #> <# foreach( var attr in p.GetCustomAttributesData() ) { EnsuresAssumeActions actions; if( AttributeTypeMap.TryGetValue( AttributeType(attr), out actions ) ) { actions.Ensures( attr, p ); } } #> <# } #> <# foreach( var p in t.GetProperties() ) { #> <# foreach( var attr in p.GetCustomAttributesData() ) { EnsuresAssumeActions actions; if( AttributeTypeMap.TryGetValue( AttributeType(attr), out actions ) ) { actions.Assume( attr, p ); } } #> <# } #> } <# } #> } } <#+ class EnsuresAssumeActions { public Action< CustomAttributeData, PropertyInfo > Ensures; public Action< CustomAttributeData, PropertyInfo > Assume; } Dictionary<Type, EnsuresAssumeActions> AttributeTypeMap = new Dictionary<Type, EnsuresAssumeActions>(); public string ClassName( Type t ) { return t.FullName; } public Type AttributeType( CustomAttributeData attrData ) { return attrData.Constructor.DeclaringType; } public string AttributeTypeName( Type t ) { string s =t.Name; s = s.Substring( 0, s.Length- "Attribute".Length); return s; } #>

Requires にしか変換パターン書いてないけど、必要に応じて足してくれればいいよね。

これで Unproven 祭りに Assume 祭りの応戦をする必要はなくなったね! code-contracs 活用して堅いサービス書いてね!、それでは see you!