ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET Web API の Swagger ドキュメントからパラメータのモデル名を削除する

ASP.NET Web API では、Swashbuckle を使って Swagger ドキュメントを作成します。
具体的な手順は、過去の記事を参照してください。
gooner.hateblo.jp

アクションメソッドに FromUri 属性を指定すると、URI のパラメータを自分で定義したクラスにバインドできます。開始と終了のコードを指定して検索できる API であれば、次のように実装できます。

[RoutePrefix("api/persons")]
public class PersonController : ApiController
{
	[Route]
	public IEnumerable<Person> Get([FromUri]PersonGetParameters model)
	{
	}
}

public class PersonGetParameters
{
	public string StartCode { get; set; }
	public string EndCode { get; set; }
}

Swagger UI で確認すると、以下のように表示されます。

f:id:TonyTonyKun:20160725153354p:plain
API の動作としては、以下のどちらの URL でもリクエストできるので間違いではないのですが、少し格好悪いので Swagger ドキュメントから「model.」を削除したくなります。

  • api/persons?model.startCode=001&model.endCode=002
  • api/persons?startCode=001&endCode=002

削除するには、2通りの対応があります。
元ネタは、GitHub の Issues に挙がっている内容です。
github.com

モデル名を含めたリクエストを許可しない

API の URL ルーティングとして、モデル名を含めたリクエストを許可しないことで、Swagger ドキュメントからも「model.」が削除されます。必要な実装は、FromUri 属性の Name プロパティに Empty をセットするだけです。

[Route]
public IEnumerable<Person> Get([FromUri(Name = "")]PersonGetParameters model)
{
}

f:id:TonyTonyKun:20160725153412p:plain

Swagger ドキュメントをカスタマイズする

Swashbuckle の IOperationFilter インターフェイスを利用すると、Swagger メタデータ プロセスのさまざまな部分をカスタマイズできる拡張ポイントが提供されます。
IOperationFilter インターフェイスを継承した MyOperationFilter クラスを実装しました。

public class Startup
{
	public void Configuration(IAppBuilder app)
	{
		var config = new HttpConfiguration();
		config.MapHttpAttributeRoutes();

		// Swashbuckle の構成
		config.EnableSwagger(c =>
		{
			c.SingleApiVersion("v1", "WebApplication4");
			c.IncludeXmlComments($@"{AppDomain.CurrentDomain.BaseDirectory}bin\WebApplication4.XML");
			c.OperationFilter<MyOperationFilter>();
		})
		.EnableSwaggerUi(c =>
		{
		});

		app.UseWebApi(config);
	}

}

Apply メソッドでパラメータ名を変換すると、Swagger ドキュメントからも「model.」が削除されます。

public class MyOperationFilter : IOperationFilter
{
	public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
	{
		if (operation.parameters != null)
		{
			foreach (var pram in operation.parameters)
			{
				// 例)"model.startCode" → "startCode" に変換
				var index = pram.name.IndexOf(".");
				if (index != -1)
				{
					pram.name = pram.name.Substring(index + 1);
				}
			}
		}
	}
}

まとめ

シンプルな API であれば、アクションメソッドに int や string などのプリミティブ型で十分対応できますが、指定できる検索条件が多くなると自分で定義したクラスにパラメータをバインドしたくなります。
その際に、モデル名を削除する方法を2通り紹介しました。

  • モデル名を含めたリクエストを許可しない
  • Swagger ドキュメントをカスタマイズする

Swagger ドキュメントをカスタマイズする方法では、重複したパラメータ名が出力される可能性があります。
1つの API で複数の URL ルーティングをサポートする必要性はないので、モデル名を含めたリクエストを許可しない方が良いのかなと思います。

Roslyn で ASP.NET Web API 向けの Code Analyzer を作ってみた

Visual Studio 2015 から追加された .NET Compiler Platform(Roslyn)を使って、ASP.NET Web API 向けの Code Analyzer を作ってみました。

環境構築

Roslyn で Code Analyzer を開発するには、Visual Studio のオプション機能と .NET Compiler Platform SDK をインストールする必要があります。Build Insider の記事が分かりやすいので、こちらを参照してください。
www.buildinsider.net
SDK と一緒に、Syntax Visualizer というツールもインストールされます。SyntaxTree をリアルタイムに表示してくれるので、実装する際に役立ちます。

プロジェクトの作成

開発環境が整うと、Visual Studio のプロジェクト テンプレートの Extensibility に Roslyn 用のテンプレートが表示されます。
f:id:TonyTonyKun:20160316060615p:plain
「Analyzer with Code Fix(NuGet + VSIX)」を選択して作成します。Analyzer プロジェクトには、クラス名に小文字が含まれていたら警告を表示するサンプルが入っています。Analyzer.Vsix プロジェクトをスタートアップにしてデバッグ実行すると、別の Visual Studio が起動するので、そこで開いたプロジェクトでコード解析とリファクタリングのコードをデバッグできます。

シナリオ

ASP.NET Web API 向けの実用的なシナリオとして、 POST のアクションメソッドの戻り値を void にすると、HttpStatusCode.NoContent(204)が返されるけどいいの?と警告を表示してみます。一般的には HttpStatusCode.Created(201)を返すことが多いので、リファクタリングの機能も提供します。
いろいろと調べていると、すでに同じ内容でブログを書いている方がいることに気が付きました。
Building Web API Visual Studio support tools with Roslyn | StrathWeb. A free flowing web tech monologue.
3年前の記事で Roslyn のコードが古いので、気にせず続けることにします。

コード解析

コードを解析するには、DiagnosticAnalyzer クラスを継承したクラスを作成します。ID やタイトルなどのルールを設定した後、Initialize() メソッドの実装が肝です。

  • RegisterXXX() メソッドがたくさんあるけど、どれを使えばいいのか?
  • Analyze () メソッドの中で、どうやってコードを解析するのか?

Roslyn の敷居が高いと思うのは、これらに関するドキュメントが少なく、何をどう書けば分からないことです。
まずは、neue さんのブログを読みましょう。
neue cc - VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)
次に、自分がやりたいことに近いサンプルコードを見つける。そして、Syntax Visualizer でひたすら頑張る。これしかなさそうです。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiAnalyzer : DiagnosticAnalyzer
{
	public const string DiagnosticId = "ApiAnalyzerId";
	internal const string Title = "POST メソッドで HttpStatusCode.NoContent(204)が返されています。";
	internal const string MessageFormat = "POST メソッドで HttpStatusCode.NoContent(204)が返されています。";
	internal const string Category = "Usage";

	internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
							DiagnosticId,
							Title, 
							MessageFormat, 
							Category, 
							DiagnosticSeverity.Warning, isEnabledByDefault: true);

	public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
	{
		get
		{
			return ImmutableArray.Create(Rule);
		}
	}

	public override void Initialize(AnalysisContext context)
	{
		context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
	}

	static void Analyze(SyntaxNodeAnalysisContext context)
	{
		// ApiController の継承クラスのみ
		var syntax = (ClassDeclarationSyntax)context.Node;
		if (syntax.BaseList == null)
		{
			return;	// 親クラスがない
		}
		if (!syntax.BaseList.Types.Any(x => x.GetFirstToken().ValueText == "ApiController"))
		{
			return; // 親クラスが ApiController ではない
		}

		// メソッドの絞り込み(1)
		// ・メソッド名が "Post" から始まるメソッド、もしくは
		// ・HttpPost 属性が付与されたメソッド
		var methods = syntax.Members.OfType<MethodDeclarationSyntax>().
				Where(x => (x.Identifier.ValueText.StartsWith("Post") ||
					x.AttributeLists.Any(a => a.Attributes.Any(s => s.Name.ToString() == "HttpPost"))));

		// メソッドの絞り込み(2)
		// ・メソッドの戻り値が "void" 型
		methods = methods.Where(x => (x.ReturnType as PredefinedTypeSyntax) != null &&
					(x.ReturnType as PredefinedTypeSyntax).IsKind(SyntaxKind.VoidKeyword));

		// 警告を表示する
		foreach (var method in methods)
		{
			var diagnostic = Diagnostic.Create(Rule, method.GetLocation());
			context.ReportDiagnostic(diagnostic);
		}
	}
}

解析ロジックにコメントを入れてあります。こんな感じで警告を出したいコードを見つけることができます。この Analyzer を動かすと警告が表示されます。
f:id:TonyTonyKun:20160316055447p:plain

リファクタリング

コードをリファクタリングするには、CodeFixProvider クラスを継承したクラスを作成します。CodeAction.Create() メソッドの引数に渡す ReplaceToHttpStatusCodeCreated() メソッドの実装が肝です。コード解析と同じく、試行錯誤しながら頑張るしかなさそうです。

[ExportCodeFixProvider("ApiCodeFixProvider", LanguageNames.CSharp), Shared]
public class ApiCodeFixProvider : CodeFixProvider
{
	internal const string Title = "HttpStatusCode.Created(201)を返すように変更する。";

	public override ImmutableArray<string> FixableDiagnosticIds
	{
		get
		{
			// DiagnosticAnalyzer と CodeFixProvider の関連づけ
			return ImmutableArray.Create(ApiAnalyzer.DiagnosticId);
		}
	}

	public sealed override FixAllProvider GetFixAllProvider()
	{
		return WellKnownFixAllProviders.BatchFixer;
	}

	public override async Task RegisterCodeFixesAsync(CodeFixContext context)
	{
		// 警告が表示されている場所を取得する
		var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
		var diagnostic = context.Diagnostics.First();
		var diagnosticSpan = diagnostic.Location.SourceSpan;

		// 警告が表示されているメソッドを取得する
		var method = (MethodDeclarationSyntax)root.FindNode(diagnosticSpan);

		// コードを置換する
		var codeAction = CodeAction.Create(Title, x => ReplaceToHttpStatusCodeCreated(context.Document, root, method, x), nameof(ApiCodeFixProvider));
		context.RegisterCodeFix(codeAction, diagnostic);
	}

	static Task<Document> ReplaceToHttpStatusCodeCreated(Document document, SyntaxNode root, MethodDeclarationSyntax method, CancellationToken cancellationToken)
	{
		// 戻り値の型は、"HttpResponseMessage" に書き換える
		var newReturnType = SyntaxFactory.ParseTypeName("HttpResponseMessage").WithTrailingTrivia(SyntaxFactory.Space);

		// "return new HttpResponseMessage(HttpStatusCode.Created);" というコードを生成する
		var expression = SyntaxFactory.ParseExpression("new HttpResponseMessage(HttpStatusCode.Created)").WithLeadingTrivia(SyntaxFactory.Space);
		var returnStatement = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword), expression, SyntaxFactory.Token(SyntaxKind.SemicolonToken));

		// メソッド本文の最後にコードを追加する
		var oldBody = method.Body;
		var statements = oldBody.Statements.Where(x => x.GetType() != typeof(ReturnStatementSyntax)).ToList();
		var syntaxListStatements = new SyntaxList<StatementSyntax>();
		syntaxListStatements = statements.Aggregate(syntaxListStatements, (current, syntaxListStatement) => current.Add(syntaxListStatement));
		syntaxListStatements = syntaxListStatements.Add(returnStatement);
		var newBody = SyntaxFactory.Block(SyntaxFactory.Token(SyntaxKind.OpenBraceToken), syntaxListStatements, SyntaxFactory.Token(SyntaxKind.CloseBraceToken));

		// メソッドのコードを書き換え
		var newMethod = SyntaxFactory.MethodDeclaration(
						method.AttributeLists,
						method.Modifiers,
						newReturnType,
						method.ExplicitInterfaceSpecifier,
						method.Identifier,
						method.TypeParameterList,
						method.ParameterList,
						method.ConstraintClauses,
						newBody,
						method.SemicolonToken);

		// 書き換えたコードにリプレイス
		var newRoot = root.ReplaceNode(method, newMethod);
		var newDocument = document.WithSyntaxRoot(newRoot);
		return Task.FromResult(newDocument);
	}
}

置換ロジックにはコメントを入れてあります。この CodeFixProvider を動かすとリファクタリングできます。
f:id:TonyTonyKun:20160316055840p:plain
プレビューでリファクタリング内容を確認したら、コードを置換することができます。
f:id:TonyTonyKun:20160316055852p:plain

まとめ

昨年の Build で、Roslyn のセッションがありました。
.NET Compiler Platform ("Roslyn"): Analyzers and the Rise of Code-Aware Libraries | Build 2015 | Channel 9
このなかで語られている Code - Aware Libraries というメッセージが、Roslyn の面白さを表していると思います。ライブラリ開発者は、Analyzer を一緒に提供することで、正しい使い方を教えてあげることができます。ドキュメントにあれこれ書くよりも確実に伝える(強制する)ことができるので、うまく活用していきたいです。

プロキシ環境下で Web API を呼び出す

プロキシ サーバーを経由してクラウド上の Web API を呼び出す方法をまとめておきます。

プロキシ環境下において、HttpWebRequest や HttpClient を使って通常通りに Web API を呼び出すと、HTTP ステータスコードの 407(Proxy Authentication Required)が返されます。そのため、プロキシ サーバーの資格情報(ユーザー名とパスワード)を設定してから、Web API を呼び出す必要があります。

インターネット オプションで設定したプロキシ サーバーの IPアドレスやポートは、WebRequest クラス の DefaultWebProxy プロパティに設定されています。ここに Credentials プロパティがあるので、資格情報を設定します。

private void Button_Click(object sender, RoutedEventArgs e)
{
	// プロキシ サーバーの資格情報をセット
	WebRequest.DefaultWebProxy.Credentials = new NetworkCredential("UserName", "Password");

	// HttpWebRequest による Web API 呼び出し
	var request = WebRequest.Create(new Uri("http://xxx.azurewebsites.net/api/values")) as HttpWebRequest;
	request.Method = "GET";
	request.Accept = "application/json";
	var response = request.GetResponse() as HttpWebResponse;
	using (var stream = response.GetResponseStream())
	using (var reader = new StreamReader(stream, Encoding.UTF8))
	{
		var responseContent = reader.ReadToEnd();
	}
}
private async void Button_Click2(object sender, RoutedEventArgs e)
{
	// プロキシ サーバーの資格情報をセット
	WebRequest.DefaultWebProxy.Credentials = new NetworkCredential("UserName", "Password");

	// HttpClient による Web API 呼び出し
	using (var client = new HttpClient())
	{
		var response = await client.GetAsync("http://xxx.azurewebsites.net/api/values");
		result = await response.Content.ReadAsStringAsync();
		response.EnsureSuccessStatusCode();
	}
}

WebRequest.DefaultWebProxy は Static なプロパティです。上記のコードでは、ボタンクリック イベントで毎回資格情報を設定していますが、ロード イベントなどで1回だけ資格情報を設定する方法もあります。

ちなみに、WCF サービスを呼び出す場合も、同じ方法でプロキシ サーバーの資格情報を設定できます。

private void Button_Click(object sender, RoutedEventArgs e)
{
	// プロキシ サーバーの資格情報をセット
	WebRequest.DefaultWebProxy.Credentials = new NetworkCredential("UserName", "Password");

	// WCF による Web サービス 呼び出し
	var binding = new BasicHttpBinding();
	var remoteAddress = "http://xxx.azurewebsites.net/Service1.svc";
	var channel = new ChannelFactory<IService1>(binding, remoteAddress); 
	var proxy = channel.CreateChannel();
	result = proxy.GetData();
}

アプリとしては、事前に資格情報を設定させておく、もしくは 407 が返されたらログイン画面で資格情報を入力させるなどの対応が考えられます。