ASP.NET Core webapiでcsvを返す

経緯

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

手順

.NET Coreのインストー

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

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

インストール完了したらPowerShelldotnet --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のオブジェクトで、ドキュメント.aspx)を読んだところ、任意の型を返しておけば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

うまくいきました!