ROMANCE DAWN for the new world

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

Mac で ASP.NET Core を動かしてみた

公式のチュートリアル手順で簡単に動かせると思っていましたが、環境構築に少し手間取ったので、まとめておきます。
Your First ASP.NET Core Application on a Mac Using Visual Studio Code — ASP.NET documentation

環境構築

最近購入した MacBook Pro を MacOS Sierra にアップグレードしたので、ほぼクリーンインストールな環境です。

1. nodejs
こちらのリンクからインストーラーをダウンロードして、インストールします。

2. Homebrew
次のコマンドで Homebrew をインストールします。

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

3. OpenSSL
次のコマンドで OpenSSL をインストールします。

brew update
brew install openssl
ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/
ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/

4. .NET Core SDK
こちらのリンクから official installer をダウンロードして、インストールします。
しかし、インストールしただけでは、ターミナルで dotnet コマンドを認識しません。
そのため、次のコマンドでパスを通す必要があります。

ln -s /usr/local/share/dotnet/dotnet /usr/local/bin/

以上で、必要最低限の環境構築が完了しました。

Hello World

これでやっと、Hello World のプログラムを動かすことができます。

mkdir hwapp
cd hwapp
dotnet new
dotnet restore
dotnet run

f:id:TonyTonyKun:20161008233309p:plain

Visual Studio Code

クロスプラットフォームなエディターの Visual Studio Code をインストールします。
code.visualstudio.com
合わせて、C# の拡張機能もインストールしておきます。
marketplace.visualstudio.com
インストールしただけでは、ターミナルで code コマンドを認識しません。
そのため、VS Code を起動して、[Command]+[Shift]+[P]キーを入力し、code コマンドをインストールします。

Shell Command: Install 'code' command in PATH

あと、Dock から VS Code を起動するには、展開した ZIP ファイルの .app ファイルを [アプリケーション] フォルダに移動する必要があります。

Yeoman

Web アプリのテンプレートを自動作成してくれる Yeoman をインストールします。
ASP.NET Core のジェネレータと Bower も合わせて、次のコマンドでインストールします。

npm install -g yo generator-aspnet bower

しかし、インストールに失敗してしまいます。
f:id:TonyTonyKun:20161008233850p:plain
どうやら、インストール先のディレクトリにアクセス権限がないようです。
ググってみると、いくつかの対応方法がありましたが、今回は sudo コマンドでインストールすることにしました。

sudo npm install -g yo generator-aspnet bower

ASP.NET Core MVC を実行する

Yeoman を使ってスキャフォールディングした ASP.NET Core MVC のアプリケーションを作成します。

yo aspnet

Web Application → Bootstrap (3.3.6) を選択して、プロジェクトを作ります。

f:id:TonyTonyKun:20161008235124p:plain
NuGet パッケージを復元してアプリを実行します。

cd webapp
dotnet restore
dotnet run

ブラウザから localhost:5000 にアクセスすると、次のような画面が表示されます。

f:id:TonyTonyKun:20161008235137p:plain

一旦アプリを終了し、次のコマンドで VS Code を起動します。

code .

ブレイクポイントを置いて実行すると、デバッグを止めることができます。

f:id:TonyTonyKun:20161009000134p:plain

まとめ

ASP.NET Core には期待しているので、Mac にも少しずつ慣れながら、試していきたいと思います。

ファイルをアップロードする API の Swagger ドキュメントを書く

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

ASP.NET Web API で実装したファイルをアップロードする API のドキュメントを Swagger で書く方法を調べました。
multipart / form-data を使って、ファイルのバイナリデータを送信して、Azure Blob Storage にアップロードするシナリオです。

[HttpPost]
[Route("upload")]
public async Task<IHttpActionResult> Upload()
{
    if (Request.Content.IsMimeMultipartContent() == false)
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
 
    var provider = await Request.Content.ReadAsMultipartAsync();
    var fileContent = provider.Contents.First(x => x.Headers.ContentDisposition.Name == JsonConvert.SerializeObject("buffer"));
    var buffer = await fileContent.ReadAsByteArrayAsync();
 
    var blob = this.container.GetBlockBlobReference("test.jpg");
    blob.Properties.ContentType = fileContent.Headers.ContentType.MediaType;
    await blob.UploadFromByteArrayAsync(buffer, 0, buffer.Length);
 
    return Ok();
}

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

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

public class UploadFileOperationFilter : IOperationFilter
{
	public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
	{
		operation.consumes.Add("multipart/form-data");
		operation.parameters = new[]
		{
			new Parameter
			{
				name = "buffer",
				@in = "formData",
				required = true,
				type = "file",
				description = "アップロードするファイル"
			},
		};
	}
}

Content-Type に multipart / form-data を追加し、パラメータを設定しています。入力形式に formData、タイプに file を指定するところが肝です。
そして、SwaggerOperationFilter 属性を使って、先ほどのアクションメソッドに UploadFileOperationFilter を指定します。

[HttpPost]
[Route("upload")]
[SwaggerOperationFilter(typeof(UploadFileOperationFilter))]
public async Task<IHttpActionResult> Upload()
{
}

Swagger ドキュメントでは、次のように表示され、参照ボタンからファイルを選択して API をテスト実行できます。

f:id:TonyTonyKun:20160920200609p:plain

まとめ

クエリパラメータや JSON のリクエストボディであれば、アクションメソッドの引数から Swashbuckle がドキュメントを作ってくれますが、ファイルアップロードの場合には、カスタムの OperationFilter を実装する必要があります。
まとまった情報がなく、トライアンドエラーした結果なので、もしかしたら別のアプローチがあるかもしれません。

追記:ASP.NET Core の場合

gooner.hateblo.jp

ASP.NET Web API で返す JSON のプロパティを指定する

ASP.NET Web API のレスポンスで JSON を返す際に、クエリパラメータで指定したプロパティだけを返したいケースがありました。コントローラークラスのアクションメソッドの戻り値の型は変更せずに、動的にシリアライズするプロパティを変更する方法をまとめておきます。

Json.NET のカスタマイズ

Json.NET には、シリアライズする型のプロパティをカスタマイズできる機能が提供されています。DefaultContractResolver クラスを継承して、指定されたプロパティだけを JSON にシリアライズする PartialPropertyContractResolver を実装します。

internal class PartialPropertyContractResolver : DefaultContractResolver
{
	private IList<string> _propertiesToSerialize = null;
	private bool _isFirstTime = true;

	public PartialPropertyContractResolver(IList<string> propertiesToSerialize)
	{
		_propertiesToSerialize = propertiesToSerialize.Select(x => ToLowerCamelCase(x)).ToList();
		_isFirstTime = true;
	}

	protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
	{
		var properties = base.CreateProperties(type, memberSerialization);
		if (_isFirstTime == true)
		{
			// 初回のみ、シリアライズするプロパティ名の判定する
			_isFirstTime = false;
			return properties.Where(p => _propertiesToSerialize.Contains(p.PropertyName)).ToList();
		}
		else
		{
			// 2回目以降は、内包している独自型のプロパティなので、すべてのプロパティをシリアライズする
			return properties;
		}
	}

	protected override string ResolvePropertyName(string propertyName)
	{
		return ToLowerCamelCase(propertyName);
	}

	private string ToLowerCamelCase(string s)
	{
		if (string.IsNullOrEmpty(s) || !char.IsUpper(s[0]))
		{
			return s;
		}

		char[] chars = s.ToCharArray();
		for (int i = 0; i < chars.Length; i++)
		{
			if (i == 1 && !char.IsUpper(chars[i]))
			{
				break;
			}

			bool hasNext = (i + 1 < chars.Length);
			if (i > 0 && hasNext && !char.IsUpper(chars[i + 1]))
			{
				break;
			}
			chars[i] = char.ToLower(chars[i], CultureInfo.InvariantCulture);
		}
		return new string(chars);
	}
}

CreateProperties メソッドが肝です。コンストラクタの引数で受け取ったプロパティ名のリストにあるものだけをシリアライズするように変更しています。
なぜ、LowerCamelCase の実装が入っているのかは、最後に記載します。この実装がなくても、指定されたプロパティだけを JSON にシリアライズすることは可能です。

カスタムの ActionResult を実装する

コントローラークラスのアクションメソッドから JSON を返してもいいのですが、指定されたプロパティだけを JSON にシリアライズする ActionResult を実装します。

public class PartialJsonResult<T> : IHttpActionResult
{
	private readonly HttpRequestMessage _request;
	private readonly T _content;
	private readonly IList<string> _propertiesToSerialize;

	public PartialJsonResult(HttpRequestMessage request, T content, IList<string> propertiesToSerialize)
	{
		_request = request;
		_content = content;
		_propertiesToSerialize = propertiesToSerialize;
	}

	public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
	{
		var response = _request.CreateResponse(HttpStatusCode.OK);
		var json = "";
		if (_propertiesToSerialize != null)
		{
			var settings = new JsonSerializerSettings { ContractResolver = new PartialPropertyContractResolver(_propertiesToSerialize) };
			json = JsonConvert.SerializeObject(_content, settings);
		}
		else
		{
			json = JsonConvert.SerializeObject(_content);
		}
		response.Content = new StringContent(json);
		response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
		response.Content.Headers.ContentType.CharSet = "utf-8";
		return Task.FromResult(response);
	}
}

Json.NET でシリアライズする際に、先ほど実装した PartialPropertyContractResolver クラスを JsonSerializerSettings に渡しています。

API を実装する

汎用的に利用できるように、コントローラーのベースクラスを用意してヘルパーメソッドを実装します。

public abstract class BaseController : ApiController
{
	protected internal virtual PartialJsonResult<T> PartialJson<T>(T content, IList<string> propertiesToSerialize)
	{
		return new PartialJsonResult<T>(Request, content, propertiesToSerialize);
	}
}

クエリパラメータで取得したい JSON プロパティを指定できる API を実装します。

[RoutePrefix("api/people")]
public class PersonController : BaseController
{
	[Route]
	public IHttpActionResult Get([FromUri]List<string> properties)
	{
		var model = new Person { Id = 1, Name = "Gooner" };
		return PartialJson(model, properties);
	}
}

API の呼び出し結果は、次のようになります。
まずは、idとnameプロパティを返します。
f:id:TonyTonyKun:20160825184754p:plain
次に、idプロパティのみを返します。
f:id:TonyTonyKun:20160825184801p:plain
どちらも、意図した JSON が返されていることが分かります。

LowerCamelCase を実装した理由

API を公開する際には、JSON を先頭小文字で扱いたいことがあります。Json.NET には、CamelCasePropertyNamesContractResolver が用意されています。そのため、CamelCasePropertyNamesContractResolver を継承して PartialPropertyContractResolver を実装したところ、シリアライズしたいプロパティ名のリストがキャッシュされてしまう問題が発生しました。
原因は、CamelCasePropertyNamesContractResolver のコンストラクタで、shareCache フラグに true を渡していることです。
github.com
仕方がないので、CamelCasePropertyNamesContractResolver を継承せずに LowerCamelCase を実装しました。

まとめ

ASP.NET Web API のレスポンスで JSON に多くのプロパティがある場合、必要なプロパティだけをコンパクトに返したいケースはあると思います。カスタムの ActionResult やコントローラークラスのヘルパーメソッドは必須ではありませんが、組み合わせて実装すると汎用的に利用することができます。