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が作れるかも。

