F#でASP.NET CoreのWeb APIを作ろう(SQLite編)

F#でASP.NET CoreのWeb APIを作ろう | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7996

の続きで、データベースにSQLiteを使ってWeb APIを作成します。
.NET Core用のSQLite は NuGet で「”Microsoft.EntityFrameworkCore.Sqlite”」があるので、これを使えばokです。
C#の.NET CoreのWeb APIプロジェクトからコピーしたあとから、再スタート。

project.jsonの修正

– “includeFiles” にF#のファイルをひとつずつ追加(面倒だけど)

– “dependencies” に4つの参照をを追加
– “Microsoft.EntityFrameworkCore”
– “Microsoft.EntityFrameworkCore.Sqlite”
– “Microsoft.EntityFrameworkCore.Sqlite.Design”
– “Microsoft.EntityFrameworkCore.Tools”

C#のときは “Microsoft.EntityFrameworkCore” が無くても動くのだけど、F#のプロジェクトには入れます。でも、入れるのが普通な気がする。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "debugType": "portable",
    "compilerName": "fsc",
    "compile": {
      "includeFiles": [
        "Models/Person.fs",
        "Data/ApplicationDbContext.fs",
        "Controllers/ValuesController.fs",
        "Controllers/PeopleController.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",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.Sqlite": "1.0.0",
    "Microsoft.EntityFrameworkCore.Sqlite.Design": "1.0.0",
    "Microsoft.EntityFrameworkCore.Tools":  "1.0.0-preview2-final"
  },

  "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%" ]
  }
}

データベース接続の設定

appsettings.json に “DefaultConnection” を追加。ローカルファイルにSQLiteのデータベースを置きます。

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Filename=./sample.db"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

StartupクラスのConfigureServicesメソッドでSQLiteに接続できるようにする。

type Startup(env:IHostingEnvironment) as this =
    do 
        let builder = new ConfigurationBuilder()
        builder.SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", true, true)
                .AddJsonFile("appsettings." + env.EnvironmentName + ".json", true)
                .AddEnvironmentVariables() |> ignore
        this.Configuration <- builder.Build()

    [<DefaultValue>]
    val mutable _Configuration:IConfigurationRoot 
    member val Configuration:IConfigurationRoot = null with get, set
    member x.ConfigureServices( services:IServiceCollection ) =
        services.AddDbContext<ApplicationDbContext>( 
            fun options ->
                options.UseSqlite(this._Configuration.GetConnectionString("DefaultConnection")) |> ignore
                ) |> ignore
        services.AddMvc() |> ignore
        ()

    member x.Configure( app:IApplicationBuilder, env:IHostingEnvironment, loggerFactory:ILoggerFactory ) =
        loggerFactory.AddConsole(this._Configuration.GetSection("Logging")) |> ignore
        loggerFactory.AddDebug() |> ignore
        app.UseMvc() |> ignore
        ()

Modelの作成

シンプルに自動生成のプロパティを使えます。

Person.cs

type Person() =
    member val Id = 0 with get, set
    member val Name = "" with get, set
    member val Age = 0 with get, set

ApplicationDbContextの作成

namespace ApiFSharp.Data
open Microsoft.EntityFrameworkCore
open ApiFSharp.Models

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) =
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = x._Person and set value = x._Person <- value
    override x.OnModelCreating( builder:ModelBuilder ) =
        base.OnModelCreating(builder)
        ()

実は、「member x.Person with …」のところで随分悩みました。
自己参照のつもりで、this を追加して this._Person で参照させると実行時に、循環参照のエラーがでます。

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) as this=
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = this._Person and set value = this._Person <- value

http://localhost:5000/api/people で、Web APIにアクセスしようとすると、System.InvalidOperationException の例外が発生しています。

Hosting environment: Production
Content root path: D:workblogsrcSQLiteWebApiSQLiteWebApiSQLiteFSharp
Now listening on: http://*:5000
Application started. Press Ctrl+C to shut down.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/api/people
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HKUCHBOFLSDB": An unhandled exception was thrown by the application.
System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed
 recursively before it was fully initialized.
   at Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions.FailInit()
   at ApiFSharp.Data.ApplicationDbContext.set_Person(DbSet`1 value)
   at Microsoft.EntityFrameworkCore.Internal.DbSetInitializer.InitializeSets(DbContext context)
   at Microsoft.EntityFrameworkCore.DbContext..ctor(DbContextOptions options)
   at ApiFSharp.Data.ApplicationDbContext..ctor(DbContextOptions`1 options)
...

これを this 使わずに

type ApplicationDbContext( options:DbContextOptions<ApplicationDbContext> ) =
    inherit DbContext( options )
    [<DefaultValue>] 
    val mutable _Person:DbSet<Person> 
    member x.Person with get() = x._Person and set value = x._Person <- value

とすると循環しないので、なんか理由があるんですかね?謎です。

Controllerの作成

まめに ignore を追加します。

[<Route("api/[controller]")>]
type PeopleController(context) =
    inherit Controller() 
    let _context:ApplicationDbContext = context

    [<HttpGet>]
    member x.Get () = 
        if isNull _context.Person then  
            new List<Person>()
        else
             _context.Person.ToList()
    [<HttpGet("{id}")>]
    member x.Get (id:int) = 
        _context.Person.SingleOrDefault( fun m -> m.Id = id )
    [<HttpPost>]
    member x.Post([<FromBody>] person:Person) =
        _context.Add( person ) |> ignore
        _context.SaveChanges() |> ignore
        person.Id
    [<HttpPut("{id}")>]
    member x.Put( id:int, [<FromBody>] person:Person) =
        if id <> person.Id then
            -1
        else 
            _context.Update( person ) |> ignore
            _context.SaveChanges() |> ignore
            person.Id
    [<HttpDelete("{id}")>]
    member x.Delete( id:int ) =
        let person = _context.Person.SingleOrDefault( fun m -> m.Id = id )
        _context.Person.Remove( person ) |> ignore
        _context.SaveChanges() |> ignore
        person.Id

ビルドして実行

実行するとこんな感じです。データベースのファイル「sample.db」は自動では作ってくれないので、何らかの形であらかじめファイルを作っておきます。

サンプルコード

サンプルコードはこちら。C#版も含めてあります。
https://1drv.ms/u/s!AmXmBbuizQkXgfwM4A7bz3cWyThD_Q

次は…

あれこれ追加が必要ですが、ASP.NET Coreな環境で、F#でWeb APIが作れることがわかりました。本当はASP.NET MVCにしたいけど、ViewページのRazorが対応していないので無理かな。このあたりは後で調べます。
開発環境は Visual Studio Codeを使う(というか、むしろ Visual Studio 2015上では開発できない)ので、LinuxはMac上でも開発&動作ができます。IISとかApacheとかに関係なく、マイクロサーバー的にWeb APIを提供できる環境ができるのはよいかもしれません。Azureに乗せたりすると、自前のなんかのプロキシとか作れるんじゃないですかね。いや、C#で書いたほうが楽かもしれませんが、もうちょっとなんか整理して、Controllerだけさくさくと実験&拡張できるようにすると、手軽なWeb APIが作れるかも。

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