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