Edited at

ASP.NET Core webapiでcsvを返す

More than 1 year has passed since last update.

元記事


経緯

DBとかにためてたデータをクエリしてcsvとしてダウンロードできるようなweb apiを作りたかった。


手順


.NET Coreのインストール

現在(2017/07/19)の最新版は.NET Core 2.0 Preview 2なのでここからダウンロードする。

インストーラを起動してぽちぽちすればインストール完了。

インストール完了したらPowerShellでdotnet --versionしましょう。

PS C:\Users\ghken> dotnet --version

2.0.0-preview2-006497

ちゃんと2.0のPreview 2が入ってるのが確認できます。


プロジェクトの生成

dotnet coreでは作成するアプリケーションの種類ごとにテンプレートが用意されているので確認してみましょう。

PS C:\Users\ghken> dotnet new

Template Instantiation Commands for .NET Core CLI

Usage: new [options]

Options:
-h, --help Displays help for this command.
-l, --list Lists templates containing the specified name. If no name is specified, lists all templates.
-n, --name The name for the output being created. If no name is specified, the name of the current directory
is used.
-o, --output Location to place the generated output.
-i, --install Installs a source or a template pack.
-u, --uninstall Uninstalls a source or a template pack.
--type Filters templates based on available types. Predefined values are "project", "item" or "other".
--force Forces content to be generated even if it would change existing files.
-lang, --language Specifies the language of the template to create.

Templates Short Name Language Tags
--------------------------------------------------------------------------------------------------------
Console Application console [C#], F#, VB Common/Console
Class library classlib [C#], F#, VB Common/Library
Unit Test Project mstest [C#], F#, VB Test/MSTest
xUnit Test Project xunit [C#], F#, VB Test/xUnit
ASP.NET Core Empty web [C#] Web/Empty
ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC
ASP.NET Core Web App (Razor Pages) razor [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
ASP.NET Core Web API webapi [C#] Web/WebAPI
Nuget Config nugetconfig Config
Web Config webconfig Config
Solution File sln Solution
Razor Page page Web/ASP.NET
MVC ViewImports viewimports Web/ASP.NET
MVC ViewStart viewstart Web/ASP.NET

Examples:
dotnet new mvc --auth Individual
dotnet new razor
dotnet new --help

今回はwebapiを使ってみます。

webapiのプロジェクトを生成するにはdotnet new webapiを実行すればいいのですが、compoer create-projectrails newと違ってプロジェクトのフォルダは生成されないのであらかじめプロジェクトのフォルダに移動したうえで実行しましょう。

PS C:\Users\ghken> mkdir csvapi

ディレクトリ: C:\Users\ghken

Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2017/07/19 17:27 csvapi

PS C:\Users\ghken> cd csvapi
PS C:\Users\ghken\csvapi> dotnet new webapi
The template "ASP.NET Core Web API" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/template-3pn for details.

Processing post-creation actions...
Running 'dotnet restore' on C:\Users\ghken\csvapi\csvapi.csproj...
Restoring packages for C:\Users\ghken\csvapi\csvapi.csproj...
Restore completed in 26.11 ms for C:\Users\ghken\csvapi\csvapi.csproj.
Generating MSBuild file C:\Users\ghken\csvapi\obj\csvapi.csproj.nuget.g.props.
Generating MSBuild file C:\Users\ghken\csvapi\obj\csvapi.csproj.nuget.g.targets.
Restore completed in 1.75 sec for C:\Users\ghken\csvapi\csvapi.csproj.

Restore succeeded.

これでプロジェクトが生成されました。

生成されたものを確認しておきましょう。

PS C:\Users\ghken\csvapi> ls

ディレクトリ: C:\Users\ghken\csvapi

Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2017/07/19 17:27 Controllers
d----- 2017/07/19 17:27 obj
d----- 2017/07/19 17:27 wwwroot
-a---- 2017/07/19 17:27 178 appsettings.Development.json
-a---- 2017/07/19 17:27 228 appsettings.json
-a---- 2017/07/19 17:27 582 csvapi.csproj
-a---- 2017/07/19 17:27 623 Program.cs
-a---- 2017/07/19 17:27 874 Startup.cs


動かしてみる

dotnet coreのアプリケーションを実行するにはdotnet runコマンドを使います。

PS C:\Users\ghken\csvapi> dotnet run

Hosting environment: Production
Content root path: C:\Users\ghken\csvapi
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

どうやらローカルの5000番ポートで何やら動き始めたようです。

linuxとかならcurl使うんですがwindowsに入れてないのでInvoke-WebRequestを使って動作確認してみましょう。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000

Invoke-WebRequest : リモート サーバーがエラーを返しました: (404) 見つかりません
発生場所 行:1 文字:1
+ Invoke-WebRequest http://localhost:5000
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

ルートにアクセスすると404が返ってきます。

何をしてるのか把握するためにコードを読んでみましょう。

Controllersってフォルダがあるのでその中を見てみるとValuesController.csというファイルがあります。

using System;

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace csvapi.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}

// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}

// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

[Route("api/[controller]")]って書いてあるので/api/valuesにアクセスしたら何か起こりそうな気がします。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api/values

StatusCode : 200
StatusDescription : OK
Content : ["value1","value2"]
RawContent : HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Date: Wed, 19 Jul 2017 08:43:11 GMT
Server: Kestrel

["value1","value2"]
Forms : {}
Headers : {[Transfer-Encoding, chunked], [Content-Type, application/json; charset=utf-8], [Date, Wed, 19 Jul 2017 08:43:11 GMT], [Server, Kestrel]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 19

contentとして["value1", "value2"]が返ってきているのでどうやらGet()が実行されたようです。


CSVを返す

Controllerのメソッドごとに返値の型がバラバラなのでこれをCSVっぽい型にすればいいんだろうか。

Controllerのメソッドの返値はIActionResultのオブジェクトで、ドキュメントを読んだところ、任意の型を返しておけばActionResultでラップしてくれるらしい。

配列とかを返せばそのままjsonでレスポンスしてくれる。

ではCSVはどうするか。

ContentResultってのがあるのでそれを使ってValuesControllerのGetを書き換えてみる。

ヘルパー関数としてContentが用意されていて、第一引数にコンテンツの文字列、第二引数にContent-Typeを入れてやればいいらしい。

public ContentResult Get()

{
return Content("hoge, fuga", "text/csv");
}

アクセスしてみる。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api/values

StatusCode : 200
StatusDescription : OK
Content : hoge, fuga
RawContent : HTTP/1.1 200 OK
Content-Length: 10
Content-Type: text/csv
Date: Wed, 19 Jul 2017 08:56:33 GMT
Server: Kestrel

hoge, fuga
Forms : {}
Headers : {[Content-Length, 10], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 08:56:33 GMT], [Server, Kestrel]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 10

無事text/csvで取得できました。


パスを変えてみる

api/valuesのままでは使いづらいのでControllerを追加して別のpathでリクエストを受けるようにしてみましょう。

今回は/apiでリクエストを受けるようなControllerを追加したいと思います。

Controller/RootController.csを作成してそれっぽく実装してみます。

using System;

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace csvapi.Controllers
{
[Route("/api")]
public class RootController : Controller
{
[HttpGet]
public ContentResult Get() => Content("hoge, fuga", "text/csv");
}
}

これで行けるか。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api

StatusCode : 200
StatusDescription : OK
Content : hoge, fuga
RawContent : HTTP/1.1 200 OK
Content-Length: 10
Content-Type: text/csv
Date: Wed, 19 Jul 2017 09:10:31 GMT
Server: Kestrel

hoge, fuga
Forms : {}
Headers : {[Content-Length, 10], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:10:31 GMT], [Server, Kestrel]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 10

成功です。


クエリストリングを扱う

条件をを受けとってそれによって加工したデータを返したいので、クエリストリングを扱う方法を調べます。

ControllerクラスのRequestにQueryプロパティが存在しているのでそれが使えそう。

keyにhogeがあればそれを、なければhogeを使って出力するようにしてみた。

public ContentResult Get()

{
var hoge = Request.Query.ContainsKey("hoge") ? Request.Query["hoge"].First() : "hoge";
return Content(hoge + ",fuga", "text/csv");
}

実行。

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api?hoge=fuga

StatusCode : 200
StatusDescription : OK
Content : fuga,fuga
RawContent : HTTP/1.1 200 OK
Content-Length: 9
Content-Type: text/csv
Date: Wed, 19 Jul 2017 09:24:35 GMT
Server: Kestrel

fuga,fuga
Forms : {}
Headers : {[Content-Length, 9], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:24:35 GMT], [Server, Kestrel]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 9

PS C:\Users\ghken> Invoke-WebRequest http://localhost:5000/api

StatusCode : 200
StatusDescription : OK
Content : hoge,fuga
RawContent : HTTP/1.1 200 OK
Content-Length: 9
Content-Type: text/csv
Date: Wed, 19 Jul 2017 09:24:40 GMT
Server: Kestrel

hoge,fuga
Forms : {}
Headers : {[Content-Length, 9], [Content-Type, text/csv], [Date, Wed, 19 Jul 2017 09:24:40 GMT], [Server, Kestrel]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 9

うまくいきました!