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#のプロジェクトには入れます。でも、入れるのが普通な気がする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | { "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
1 2 3 4 5 6 7 8 9 10 11 12 13 | { "ConnectionStrings" : { "DefaultConnection" : "Filename=./sample.db" }, "Logging" : { "IncludeScopes" : false , "LogLevel" : { "Default" : "Debug" , "System" : "Information" , "Microsoft" : "Information" } } } |
StartupクラスのConfigureServicesメソッドでSQLiteに接続できるようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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
1 2 3 4 | type Person() = member val Id = 0 with get, set member val Name = "" with get, set member val Age = 0 with get, set |
ApplicationDbContextの作成
1 2 3 4 5 6 7 8 9 10 11 12 | 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 で参照させると実行時に、循環参照のエラーがでます。
1 2 3 4 5 | 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 の例外が発生しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 使わずに
1 2 3 4 5 | 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 を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | [<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が作れるかも。