F#でASP.NET CoreのWeb APIを作ろう

なぜ、F#で作るのかはさておき、C#のWeb APIアプリをF#にコンバートしてみます。本当のところは、ASP.NET Core MVCのほうをF#に対応させてみたかったのですが、Razor構文がF#に(たぶん)対応していないので無理ってことで、ViewページのないWeb APIで試しています。まあ、F#の関数型なところがステートレスなHTTPプロトコルに合っているんじゃないか、常々思ってはいるのですが。

いわゆる、ASP.NET Core Web Application(.NET Core) をF#に直します。

単純なWebアプリのほうは、いくつかサンプルが見つかるのですが、Web APIのコンバートは見つからなかったので参考になると思います。先行きは、SQLiteを使って、データベースのアクセスまで作りたいところ。

Visual Studio Codeの準備

実は、F#の.NET Coreは、Visual Studio 2015は対応していません。コードのコンバートなのでちまちまとテキストエディタを使ってもよいのですが、さすがにインテリセンスがないとF#のコードは組みづらい。

Visual Studio Code
https://www.visualstudio.com/ja-jp/products/code-vs.aspx

を使うと、.NET CoreのF#でもインテリセンスが使えるようになります。ちなみに、LinuxとMacでもF#の.NET Core環境が使えます。

Visual Studio Codeをダウンロード&インストールして「表示」→「Extentions(拡張)」から、「Ionide-fsharp」をインストールします。
Windows版の場合は、Ionide-fsharpだけでよいのですが、LinuxとMacの場合は別途 mono もインストールします。拡張機能が mono を使っているからだと思うのだけど、そのうち .NET Coreに統一されるかもしれません。

インストール後に「有効」にして、VSCode を再起動すると、F#のコードでインテリセンスが使えるようになります。

C#の.NET CoreのWeb APIプロジェクトからコピー

Visual Studio 2015で.NET CoreのWeb APIプロジェクトを作って、C#のコード(Startup.cs、Program.cs、ValuesController.cs)以外のファイルをごっそりコピーします。

project.jsonの修正

コンソール版のF#コードを「dotnet new –lang F#」で作成して、2つのproject.jsonをうまくマージします。
F#の場合は、ビルドするときに前方参照が必要なので、”buildOptions” – “compile” – “includeFiles” にビルドする順番ファイル名を書いておきます。これが非常に面倒なんですが…先行きはどうなんでしょう?

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "debugType": "portable",
    "compilerName": "fsc",
    "compile": {
      "includeFiles": [
        "Controllers/ValuesController.fs",
        "Startup.fs",
        "Program.fs"
      ]
    }
  },
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "dotnet-compile-fsc":"1.0.0-preview2-*"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ],
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
      }
    }
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0"
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "Views",
      "Areas/**/Views",
      "appsettings.json",
      "web.config"
    ]
  },

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

コードをF#に作り変える

3つのC#のファイルをF#にコンバートします。
大体、1対1でコンバートができるのですが、F#のほうが型チェックが厳しいので、戻り値が void になる関数はまめに ignore していきます。戻り値が推論されて、実行時にメソッドが見つからなくてエラーになるのがハマりどころです。

Startup.cs

namespace ApiFSharp

open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging

type Startup(env:IHostingEnvironment) =
    let mutable _Configuration:IConfigurationRoot = null
    do 
        let builder = new ConfigurationBuilder()
        builder.SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", true, true)
                .AddJsonFile("appsettings." + env.EnvironmentName + ".json", true)
                .AddEnvironmentVariables() |> ignore
        _Configuration <- builder.Build()
    member x.Cnfiguration 
        with get() =  _Configuration 
        and set( value ) = _Configuration <- value
    member x.ConfigureServices( services:IServiceCollection ) =
        services.AddMvc() |> ignore
        ()
    member x.Configure( app:IApplicationBuilder, env:IHostingEnvironment, loggerFactory:ILoggerFactory ) =
        loggerFactory.AddConsole( _Configuration.GetSection("Logging")) |> ignore
        loggerFactory.AddDebug() |> ignore
        app.UseMvc() |> ignore
        ()

Web APIをどのPCからも受け付けられるように UseUrlsメソッドで「”http://*:5000″」のように追加しておきます。こうすると、Linux仮想マシンでWeb APIを動かして、PCのブラウザでチェックする、ということができます。

Program.fs

open System
open System.Collections.Generic
open System.IO
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Builder
open ApiFSharp

[<EntryPoint>]
let Main argv = 
    let host = new WebHostBuilder()
    let ihost = 
        host.UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .UseUrls("http://*:5000")
            .Build()
    ihost.Run()
    0

Startup.cs と Program.fs は定番の処理なので、Web APIのプロジェクトテンプレートを作ってしまえばよいと思います。
実際に手を入れていくのは、ValuesControllerクラスです。

ValuesController.cs

namespace ApiFSharp.Controllers 

open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc

[<Route("api/[controller]")>]
type ValuesController() =
    inherit Controller() 

    [<HttpGet>]
    member x.Get () = 
        ["value1"; "value2" ]
    [<HttpGet("{id}")>]
    member x.Get (id:int) = 
        "value"
    [<HttpPost>]
    member x.Post([<FromBody>] value:string) =
        ()
    [<HttpPut("{id}")>]
    member x.Put( id:int, [<FromBody>] value:string) =
        ()
    [<HttpDelete("{id}")>]
    member x.Delete( id:int ) =
        ()

データベースにアクセスしていないので、固定文字列しか返していないけど、SQLite を使ってデータアクセスを作れば結構使えるようになるかなと。

ビルドして実行する

リストアして、ビルドして、実行します。

dotnet restore
dotnet build
dotnet run

VSCodeで、Shift+Ctrl+B を押すとビルドができます。

実行する

PowerShell で dotnet run してブラウザで http://localhost:5000/api/values にアクセスします。

ちなみに Ubuntu 上の VSCode を使って F# プログラミングはこんな感じ。

固定の文字列を返すだけとか、計算するだけだとふつうに HttpListener を使って返すだけでも十分だろうという気もしますが、データベースアクセス付け加えたり、認証とかクッキーとかを使ってアクセス制限を考えたり、Controllerクラスのテストなどを考えると、ASP.NET Core を利用した Web API を F# で作るのもアリかな…という無理矢理な発想で :)

サンプルコード

サンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfwK1qbhxJOHVP0bxQ

カテゴリー: ASP.NET, F# パーマリンク