【ASP.NET Core】Blazor を試す (Blazor Server)
はじめに
気になりつつも試せていなかった Blazor 。
ASP.NET Core Preview 7 でアップデートがたくさん入ったっぽいこともあり、試してみることにしました。
- ASP.NET Core Updates in .NET 5 Preview 7 | ASP.NET Blog
- Blazor | Build client web apps with C# | .NET
実際使うとすると、既存のプロジェクトに追加(置き換え)していくことになると思うので、 MVC プロジェクトを作り、そこに必要なファイルを追加していく形で進めます。
Blazor には Blazor Server と Blazor WebAssembly があるのですが、今回 PWA などを試さないこと、 Blazor Server の方がわかりやすい( Razor などを使ったプロジェクトに近いため)気がしたので、今回は Blazor Server を試します。
Environments
Base project
まず Empty テンプレートで ASP.NET Core のプロジェクトを作り、 Controller や Razor などを追加しておきます。
Startup.cs
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace BlazorSample { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseStaticFiles(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
HomeController.cs
using Microsoft.AspNetCore.Mvc; using Models; namespace Controllers { public class HomeController: Controller { [Route("/")] public ActionResult Index() { return View("Views/Index.cshtml"); } } }
Product.cs
namespace Models { public class Product { public int Id { get; set; } public string Name { get; set; } } }
_ViewStart.cshtml
@{ Layout = "_Layout"; }
_Layout.cshtml
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>@ViewData["Title"]</title> </head> <body> @RenderBody() </body> </html>
Index.cshtml
@{ ViewData["Title"] = "Home Page"; } <h2>This is Index.cshtml</h2>
Blazor を追加する
下記の記事をもとに Blazor を使うのに必要な物を追加していきます。
middleware の追加、 Blazors SignalR hub のマッピング
Startup.cs
... public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddControllers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseStaticFiles(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapBlazorHub(); }); } ...
_Imports.razor
@using System @using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using BlazorSample
BlzrSample.razor
<h1>Hello @Name!</h1> <p>This is Blazor Components.</p> <div>@SampleInfo</div> <button @onclick="ShowConsole">Click</button> @code { [Parameter] public string Name {get; set;} [Parameter] public string SampleInfo { get; set; } public string GetName(){ var name = $"{Name} {DateTime.Now}"; Console.WriteLine(name); return name; } public void ShowConsole() { SampleInfo = GetName(); Console.WriteLine($"{SampleInfo}"); } }
Index.cshtml
@using BlazorSample.Views @{ ViewData["Title"] = "Home Page"; } <h2>This is Index.cshtml</h2> @(await Html.RenderComponentAsync<BlzrSample>(RenderMode.ServerPrerendered, new { Name = "BlazorSample" }))
Result
ボタンをクリックすると div 要素の値が変わります。
IE11
いい加減フロントエンド関連のことを試すとき、まず IE を考えるのをやめたいところですが。。。
ともかく、 IE11 で開くと、 Blazor の要素は表示されるものの、ボタンをクリックしても動作しません。
対策は色々あると思いますが、その一つである Blazor.Polyfill を試します。
- GitHub - Daddoon/Blazor.Polyfill: Polyfills for Blazor (for Internet Explorer 11 support and some other browsers)
- ASP.NET Core Blazor supported platforms | Microsoft Docs
_Layout.cshtml
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>@ViewData["Title"]</title> <base href="/"> <script type="text/javascript"> if (/MSIE \d|Trident.*rv:/.test(navigator.userAgent)) { document.write('<script src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.closest%2CIntersectionObserver%2Cdocument.querySelector%2Cfeatures=Array.prototype.forEach%2CNodeList.prototype.forEach"><\/script>'); document.write('<script src="js/blazor.polyfill.min.js"><\/script>'); } </script> </head> <body> @RenderBody() <script src="_framework/blazor.server.js"></script> </body> </html>
base タグを忘れるとエラーになるので注意が必要です。
DOM 操作
どうも C#(Blazor) からは JavaScript で行うような DOM 操作を直接行うことはできないようです。
( @foreach などを使ってリストの数だけ DOM を生成する、とかはできますが)
@* 「Numbers」の要素を増減すると div の数が変わる *@ @foreach(var n in Numbers) { <div>@SampleInfo</div> }
ということで、そのような操作がしたい場合は JavaScript を呼び出す必要があります。
JavaScript とのやり取り
JavaScript を Blazor から呼び出す
IJSRuntime を使って JavaScript を呼び出すことができます。
普段 webpack や TypeScript を使っているため、それらを追加します。
Samples
package.json
{ "dependencies": { "ts-loader": "^8.0.2", "tsc": "^1.20150623.0", "typescript": "^3.9.7", "webpack": "^4.44.1", "webpack-cli": "^3.3.12" } }
webpack.config.js
var path = require('path'); module.exports = { mode: 'development', entry: { 'blazorSample': './wwwroot/ts/blazor-sample.ts', }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, resolve: { extensions: [ '.tsx', '.ts', '.js' ] }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, './wwwroot/js'), library: 'Page', libraryTarget: 'umd' } };
blazor-sample.ts
export function callFromBlazor(name: string): string { console.log("Hello Blazor"); return `Hello Blazor ${name}`; }
BlzrSample.razor
@inject IJSRuntime JSRuntime; ... <button @onclick="CallFromBlazor">Click2</button> @code { ... private async Task CallFromBlazor() { var result = await JSRuntime.InvokeAsync<string>("Page.callFromBlazor", SampleInfo); Console.WriteLine(result); } }
JavaScript から C# の static メソッドを呼ぶ
C# の static メソッドは DotNet.invokeMethodAsync を使って呼び出すことができます。
まずは Blazor の型定義を追加します。
npm install --save @types/blazor__javascript-interop
blazor「__」javascript ??と気にならなくはないのですが、ほかにそれらしきものが見つからなかったので。
blazor-sample.ts
... export function callStaticFromJs() { // 1. Assembly name, 2. Method name, 3. Arguments DotNet.invokeMethodAsync("BlazorSample", "CallStaticFromJs", "FromJS") .then(result => console.log(result)); }
BlzrSample.razor
... <button onclick="Page.callStaticFromJs()">Click3</button> @code { ... // must add JSInvokable and set public static method. [JSInvokable] public static async Task<string> CallStaticFromJs(string message) { Console.WriteLine($"Hello JS {message}"); return await Task.FromResult("Hello static"); } ...
JavaScript から C# のインスタンスメソッドを呼ぶ
C# のインスタンスメソッドを呼ぶには、まず C# のメソッド内でインスタンスを生成し、それを JavaScript に渡します。
HelloHelper.cs
using Microsoft.JSInterop; public class HelloHelper { public HelloHelper(string name) { Name = name; } public string Name { get; set; } // call from JavaScript [JSInvokable] public string SayHello() => $"Hello, {Name}!"; }
blazor-sample.ts
... // argument: HelloHelper instance export function callInstanceFromJs(dotnetHelper: any) { return dotnetHelper.invokeMethodAsync('SayHello') .then((r: any) => console.log(r)); }
BlzrSample.razor
@inject IJSRuntime JSRuntime; ... <button @onclick="CallInstanceFromJs">Click4</button> @code { ... public async Task CallInstanceFromJs() { using(var helperRef = DotNetObjectReference.Create(new HelloHelper("Js instance"))) { await JSRuntime.InvokeAsync<string>( "Page.callInstanceFromJs", helperRef); } } ...
今回は using 句を使っているわけですが、実際にはインスタンスの寿命の関係から生成するメソッドと破棄するメソッドは分けることになるのかな~と思っています。
サーバー側のメソッドを呼ぶ
では サーバー側のメソッドを呼ぶことはできるでしょうか。
と思ったら、実は Blazor でも DI が使えると( IJSRuntime も Inject されてましたね)。
ということで、サーバー側と同じように Service クラスなどを Inject すれば OK です。
IBlazorService.cs
using System.Threading.Tasks; using Models; namespace Services { public interface IBlazorService { Task<Product> GetMessageAsync(string Name); } }
BlazorService.cs
using System.Threading.Tasks; using Models; namespace Services { public class BlazorService: IBlazorService { public async Task<Product> GetMessageAsync(string name) { return await Task.FromResult(new Product { Id = 2, Name = name, }); } } }
Startup.cs
... public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddControllers(); services.AddScoped<IBlazorService, BlazorService>(); } ...
BlzrSample.razor
@inject Services.IBlazorService _blazor ... <button @onclick="CallServerside">Click5</button> @code { ... public async Task CallServerside() { var result = await _blazor.GetMessageAsync(Name); if (result == null) { Console.WriteLine("Cannot get message"); return; } Console.WriteLine($"Success ID: {result.Id} Name: {result.Name}"); } }
おわりに
以前 Razor も試しました が、動的に処理を実行できる分 Blazor の方が幅が広がりますね。
とはいえ DOM 操作など JavaScript でしかできない処理があるため、うまく住み分けしないと中途半端になってしまいそうですね。
WebAssembly の方も気になるところです。
【.NET】Console Appを試す
はじめに
これまで ASP.NET Core のアプリは作ってきたものの、よく考えると Console アプリってあまり作ったことないよな~。と思ったので試してみることにしました。
気になるところとしては、 ASP.NET Core のように DI やログ出力などできるのか、というところなので、順に試していくことにします。
Environments
- .NET Core ver.5.0.100-preview.7.20366.6
元のプロジェクト
とりあえず Console アプリを作ります。
Program.cs
using System; namespace ConsoleSample { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
設定ファイルの読み込み
Database の接続文字列などは設定ファイルを使って読み込みたいところ。
当然ながら StreamReader などを使って通常のファイル読み込みのように JSON ファイルを読み込むことはできるのですが、 ASP.NET Core のようにもう少しシンプルな方法はないでしょうか。
Startup.cs
... private readonly IConfiguration configuration; public Startup(IHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", false, true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, true) .AddEnvironmentVariables(); configuration = builder.Build(); } ...
と思っていたら、 ConfigurationBuilder がそのまま使えるそうです。
Install
- Microsoft.Extensions.Configuration ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Configuration.FileExtensions ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Hosting ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Configuration.Json ver.5.0.0-preview.7.20364.11
Samples
NuGet でインストールができたら、読み込むための JSON ファイルと、読み込む機能を追加します。
appsettings.json
{ "ConnectionStrings": "Host=localhost;Port=5432;Database=Example;Username=postgres;Password=example" }
appsettings.Development.json
{ "Message": "Hello Development" }
appsettings.Production.json
{ "Message": "Hello Production" }
Program.cs
using System; using Microsoft.Extensions.Configuration; namespace ConsoleSample { class Program { static void Main(string[] args) { var config = GetConfiguration(); Console.WriteLine(config["ConnectionStrings"]); Console.WriteLine("Hello World!"); } private static IConfiguration GetConfiguration() { var builder = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", false, true) .AddEnvironmentVariables(); return builder.Build(); } } }
- ConfigurationBuilder クラス (Microsoft.Extensions.Configuration) | Microsoft Docs
- FileConfigurationExtensions.SetBasePath(IConfigurationBuilder, String) メソッド (Microsoft.Extensions.Configuration) | Microsoft Docs
なお Base Path に「System.IO.Directory.GetCurrentDirectory()」を設定することもできます。
EnvironmentName を取得する
「appsettings.json」については読み込みができましたが、デバッグ時には「appsettings.Development.json」を、リリースビルドでは「appsettings.Production.json」を読み込みたい。
ASP.NET Core であれば、 IHostEnvironment の EnvironmentName から受け取ることができます。
この値はどうやら Visual Studio などで登録しておけば同じようなことができるようです。
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
が、プロジェクト作成のたびに登録するのはなぁ。。。(もっとシンプルな方法があるのかもしれませんが)
ということで、 #if ディレクティブを使うことにしました。
private static IConfiguration GetConfiguration() { var builder = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", false, true) #if DEBUG .AddJsonFile($"appsettings.Development.json", false, true) #else .AddJsonFile($"appsettings.Production.json", false, true) #endif .AddEnvironmentVariables(); return builder.Build(); }
正直いまいち感がないではないのですが、とにかく環境別で切り替えることができました。
DI(Dependency Injection)
DI も ASP.NET Core と同じものが使えます。
Install
Samples
IProductService.cs
namespace Products { public interface IProductService { } }
ProductService.cs
namespace Products { public class ProductService: IProductService { } }
MainController.cs
using System; using System.Threading.Tasks; using Products; namespace Controllers { public class MainController { private readonly IProductService _product; public MainController(IProductService product) { _product = product; } public async Task StartAsync() { await Task.Run(() => Console.WriteLine(_product == null)); } } }
Program.cs
using System; using System.Threading.Tasks; using Controllers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Products; namespace ConsoleSample { class Program { static async Task Main(string[] args) { ... var servicesProvider = BuildDi(); using (servicesProvider as IDisposable) { var mainController = servicesProvider.GetRequiredService<MainController>(); await mainController.StartAsync(); } } ... private static IServiceProvider BuildDi() { var services = new ServiceCollection(); services.AddTransient<MainController>(); services.AddScoped<IProductService, ProductService>(); return services.BuildServiceProvider(); } } }
IConfiguration のインジェクション
ASP.NET Core であれば、Controller クラスからでも Configuration の値を参照できます。
これは Microsoft.AspNetCore.Mvc.Controller を継承しているからだと思いますが、 Console アプリの場合は自分で IServiceProvider に渡す必要があります。
Program.cs
... private static IServiceProvider BuildDi() { var services = new ServiceCollection(); services.AddSingleton<IConfiguration>(GetConfiguration()); services.AddTransient<MainController>(); services.AddScoped<IProductService, ProductService>(); return services.BuildServiceProvider(); } ...
ということで Configuration の値を参照できるようになりました。
MainController.cs
using System; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Products; namespace Controllers { public class MainController { private readonly IProductService _product; public MainController(IConfiguration config, IProductService product) { Console.WriteLine(config["Message"]); // <- print "Hello Development" _product = product; }
Entity Framework Core を使う
DI が同じなので当然といえば当然ですが、 Entity Framework Core も ASP.NET Core と同じように使うことができます。
Install
- Microsoft.EntityFrameworkCore ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Relational ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Abstractions ver.5.0.0-preview.7.20365.15
- Npgsql.EntityFrameworkCore.PostgreSQL ver.5.0.0-preview7-ci.20200722t163648
ConsoleSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace Models { public class ConsoleSampleContext: DbContext { public ConsoleSampleContext(DbContextOptions<ConsoleSampleContext> options) :base(options) { } } }
Program.cs
... private static IServiceProvider BuildDi() { var config = GetConfiguration(); var services = new ServiceCollection(); services.AddSingleton<IConfiguration>(config); services.AddDbContext<ConsoleSampleContext>(options => options.UseNpgsql(config["ConnectionStrings"])); services.AddTransient<MainController>(); services.AddScoped<IProductService, ProductService>(); return services.BuildServiceProvider(); } ...
ログ出力(NLog)
NLog を使ってログ出力します。
Install
- NLog ver.4.7.3
- NLog.Extensions.Logging ver.1.6.4
Samples
nlog.config
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true"> <targets> <target xsi:type="Console" name="outputconsole" layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" /> <target xsi:type="File" name="outputfile" fileName="C:\tmp\logs\ConsoleSample\${date:format=yyyy}\${date:format=MM}\${shortdate}.log" layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" /> </targets> <rules> <logger name="*" minlevel="Debug" writeTo="outputconsole" /> <!--Microsoft.* のクラスの Info レベル以下のログはスキップ--> <logger name="Microsoft.*" maxLevel="Info" final="true" /> <logger name="*" minlevel="Debug" writeTo="outputfile" /> </rules> </nlog>
ConsoleSample.csproj
... <ItemGroup> <Content Include="nlog.config"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="appsettings.Development.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="appsettings.Production.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> </Project>
NLog ver.5.0.0-beta11
ほぼドキュメント通りなのですが、一点注意が。
NLog ver.5.0.0-beta11 で試したところ、依存関係の解決に失敗してエラーになりました。
Attempt by method 'NLog.Extensions.Logging.ConfigureExtensions.CreateNLogLoggerProvider(System.IServiceProvider, Microsoft.Extensions.Configuration.IConfiguration, NLog.Extensions.Logging.NLogProviderOptions, NLog.LogFactory)' to access method 'NLog.LogManager.get_LogFactory()' failed.
おそらく ver.5.0.0 で何か変わったのかなぁ、と思っているのですが、少なくとも今(2020-08-09)、上記のコードを試す場合は ver.4.7.3 を使うのがよいと思います。
おわりに
ASP.NET Core は色々特別に作られた機能があるために DI などが実現できているのだろうな~と思っていたのですが、結構 Console アプリでもそのまま使えるのだとわかったのが今回の収穫です。
Angular などもそうだと思うのですが、「複雑でよくわからない一つの塊」ではなく、一つ一つの小さな機能(それでも DI などは相当大きいとは思いますが)が組み合わさって作られているのだ、というまぁ当たり前といえば当たり前のことに気づいたというか。
今後それぞれもう少し突っ込んでみていきたいところ。
【C#】【EntityFrameworkCore】 null 許容参照型を試す
はじめに
気になりつつも試せていなかった、 null 許容参照型 (nullable reference types) を試してみます。
プロジェクトは前回のものを使います。
Environments
- .NET 5: ver.5.0.100-preview.7.20366.6
- Microsoft.EntityFrameworkCore: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Design: ver.5.0.0-preview.7.20365.15
- Npgsql.EntityFrameworkCore.PostgreSQL: ver.5.0.0-preview7-ci.20200722t163648
- Microsoft.EntityFrameworkCore.Abstractions: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Relational: ver.5.0.0-preview.7.20365.15
- Microsoft.AspNetCore.Mvc.NewtonsoftJson: ver.5.0.0-preview.7.20365.19
準備
null 許容参照型を使うにはプロジェクトの C# バージョンを 8.0 以上にすること、また Nullable を有効にする必要があります。
CodeFirstSample.csproj
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> ...
Nullable を有効にすると、大量の Warning が出てきます。
null 許容参照型について
C# 8 以前、 null 許容にできるのは値型だけでした。
int nonNullNumber = 0; nonNullNumber = null; // <- compile error int? nullableNumber = 0; nullableNumber = null; // <- OK // null 許容の値型を通常の値型に代入するにはキャストが必要 nonNullNumber = (int)nullableNumber;
c# 8 以降は参照型についても null になりうるか、そうでないかを区別できるようになりました。
using System; using Models; namespace CSharpEightSample { class Program { static void Main(string[] args) { // Nullable instance User? nullableUser = null; // <- OK // Non-nullable instance User user = null; // <- Warning } } }
残念ながら(後方互換性のため)コンパイルエラーではなく Warning のみではありますが。
IL
さて、この null 許容型と非 null 許容型のインスタンスですが、実行時の違いはあるのでしょうか。
ILSpy Visual Studio Code Extension( https://github.com/icsharpcode/ilspy-vscode )を使って IL を覗いてみます。
.class /* 02000005 */ private auto ansi beforefieldinit CSharpEightSample.Program extends [System.Runtime]System.Object { // Methods .method /* 06000005 */ private hidebysig static void Main ( string[] args ) cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 01 00 00 ) // Method begins at RVA 0x2094 // Code size 6 (0x6) .maxstack 1 .entrypoint .locals /* 11000001 */ init ( [0] class Models.User, [1] class Models.User ) IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 IL_0003: ldnull IL_0004: stloc.1 IL_0005: ret } // end of method Program::Main ...
ローカル変数においては特に違いがなさそうです。
プロパティはどうでしょうか。
namespace CSharpEightSample { public class Sample { public string Name { get; set; } = ""; public string? NullableName {get; set; } } class Program { static void Main(string[] args) { } } }
.class /* 02000005 */ public auto ansi beforefieldinit CSharpEightSample.Sample extends [System.Runtime]System.Object { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 01 00 00 ) .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 00 00 00 ) // Fields .field /* 04000003 */ private string '<Name>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) .field /* 04000004 */ private string '<NullableName>k__BackingField' .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) // Methods .method /* 06000005 */ public hidebysig specialname instance string get_Name () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2092 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_0006: ret } // end of method Sample::get_Name .method /* 06000006 */ public hidebysig specialname instance void set_Name ( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x209a // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_0007: ret } // end of method Sample::set_Name .method /* 06000007 */ public hidebysig specialname instance string get_NullableName () cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20a3 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */ IL_0006: ret } // end of method Sample::get_NullableName .method /* 06000008 */ public hidebysig specialname instance void set_NullableName ( string 'value' ) cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20ab // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */ IL_0007: ret } // end of method Sample::set_NullableName .method /* 06000009 */ public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20b4 // Code size 19 (0x13) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldstr "" /* 70000001 */ IL_0006: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_000b: ldarg.0 IL_000c: call instance void [System.Runtime]System.Object::.ctor() /* 0A00000F */ IL_0011: nop IL_0012: ret } // end of method Sample::.ctor // Properties .property /* 17000001 */ instance string Name() { .get instance string CSharpEightSample.Sample::get_Name() .set instance void CSharpEightSample.Sample::set_Name(string) } .property /* 17000002 */ instance string NullableName() { .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .get instance string CSharpEightSample.Sample::get_NullableName() .set instance void CSharpEightSample.Sample::set_NullableName(string) } } // end of class CSharpEightSample.Sample
「NullableName」の方には「System.Runtime.CompilerServices.NullableAttribute」が付与されていますね。
default!
例えば非 null 許容の string 型のインスタンスがあるとして、デフォルト値として空文字を設定するか、「default!」を渡すことで Warning を回避できます。
string sample = ""; // <- OK string sample2 = default! // <- OK
さて、「sample2」にはデフォルトで何が入っているのか?というと、やっぱり null でした。
ということで、「default!」はただ Warning を抑えるためだけに使える、といった印象を受けました。
Generics
Generics はどうでしょうか。
まず、下記のように書くことはできません。
... public class Sample<T> where T: class { public T Name { get; set; } = default!; public T? NullableName {get; set; } // <- compile error } ...
「T」に対して class または struct の制約がつけられるのであれば、「T?」を使うことができます。
... public class Sample<T> where T: class { public T Name { get; set; } = default!; public T? NullableName {get; set; } // <- OK } ...
また「T」に null 許容の型を指定したい場合は「class ?」を使うことができます。
... public class Sample<T> where T: class ? { public T Name { get; set; } = default!; public T NullableName {get; set; } = default!; } class Program { static void Main(string[] args) { var s = new Sample<string?>(); // <- OK } } ...
- Constraints on type parameters - C# Programming Guide | Microsoft Docs
- null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
またプロパティやメソッドに対しては属性を使う、といった方法もあります。
... public class Sample<T> { public T Name { get; set; } = default!; [AllowNull] public T NullableName {get; set; } = default!; } class Program { static void Main(string[] args) { var s = new Sample<string>(); s.NullableName = null; // <- OK } ...
... public class Sample<T> { public T Name { get; set; } = default!; [AllowNull] public T NullableName {get; set; } = default!; [return: MaybeNull] public T GetNull() { return default(T); } } class Program { static void Main(string[] args) { var s = new Sample<string>(); s.NullableName = null; Console.WriteLine((s.GetNull() == null)); // <- true } ...
Entity FrameWork Core
DbContext
DbContext では、「DbSet<T>」のインスタンスに対して 「Set<T>()」を使い、 null 許容を避けることができます。
using Microsoft.EntityFrameworkCore; namespace Models { public class CodeFirstSampleContext: DbContext { public CodeFirstSampleContext(DbContextOptions<CodeFirstSampleContext> options) : base(options) { } public DbSet<Workflow> Workflows => Set<Workflow>(); public DbSet<WorkflowReader> WorkflowReaders => Set<WorkflowReader>(); } }
Model
ではモデルクラスはどうでしょうか。
こちらは特に null 許容を避けるメソッドなどが用意されているわけではないようなので、 Database のテーブルなどから null 許容にすべきかどうかを判断する、という必要があります。
null 許容・非 null 許容参照型はマイグレーションファイルに影響を与えるか
答えはイエスです。
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Models { public class Sample { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public string? Name { get; set; } = "hello"; } }
これでマイグレーションファイルを作ると、デフォルト値を設定していても「Name」カラムは null 許容となります。
... protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Samples", columns: table => new { Id = table.Column<int>(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Name = table.Column<string>(type: "text", nullable: true) }, constraints: table => { table.PrimaryKey("PK_Samples", x => x.Id); }); } ...
今のところ、できる限り全プロパティを非 null 許容にすべきか?というところはわかっていないのですが、まずはできる限り null 許容を避けてみようかな、とは思っています。
参照
- Nullable reference types | Microsoft Docs
- Design with nullable reference types | Microsoft Docs
- Nullable value types - C# reference | Microsoft Docs
- Essential .NET - C# 8.0 and Nullable Reference Types | Microsoft Docs
- Constraints on type parameters - C# Programming Guide | Microsoft Docs
- null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- C# Reserved attributes: Nullable static analysis | Microsoft Docs
- Working with nullable reference types - EF Core | Microsoft Docs
【ASP.NET Core】【Entity Framework Core】Code first / DB first ってみる
はじめに
Entity Framework Core でコードからデータベースのテーブルを生成する Code first、データベースからコードを生成する DB first をやってみたよというお話。
Environments
- .NET 5: ver.5.0.100-preview.7.20366.6
- Microsoft.EntityFrameworkCore: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Design: ver.5.0.0-preview.7.20365.15
- Npgsql.EntityFrameworkCore.PostgreSQL: ver.5.0.0-preview7-ci.20200722t163648
- Microsoft.EntityFrameworkCore.Abstractions: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Relational: ver.5.0.0-preview.7.20365.15
- Microsoft.AspNetCore.Mvc.NewtonsoftJson: ver.5.0.0-preview.7.20365.19
※Microsoft.EntityFrameworkCore.Abstractions と Microsoft.EntityFrameworkCore.Relational は Microsoft.EntityFrameworkCore (だったと思う)が依存しているとリストア時にエラーを出したので追加しました。
※個人的な興味から .NET 5 を使ってますが、現状(2020-07-24 時点)では .NET Core 3.1 と書き方などほとんど変わっていないと思います。
Code first
ではまず Code first から。
プロジェクトを作る
空で ASP.NET Core のプロジェクトを作ります。
dotnet new empty -n CodeFiirstSample
NuGet でパッケージを追加しておきます。
- Microsoft.EntityFrameworkCore
- Npgsql.EntityFrameworkCore.PostgreSQL
- Microsoft.EntityFrameworkCore.Abstractions
- Microsoft.EntityFrameworkCore.Relational
Samples
接続文字列やテーブルに対応するクラスなどを追加します。
appsettings.Development.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "ConnectionStrings": "Host=localhost;Port=5432;Database=WorkflowSample;Username=postgres;Password=XXX" }
Models/Workflow.cs
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Models { [Table("Workflow")] public class Workflow { // 主キー [Key] // Auto increament [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set;} // not null [Required] public int ProductId { get; set; } [Required] [Column("CreateUserMail", TypeName="text")] public string CreateUserMail { get; set;} [Column(TypeName="timestamp with time zone")] public DateTime? CirculationLimit { get; set; } [Column(TypeName="timestamp with time zone")] // 更新のたびに現在時刻を入れたい [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime LastUpdateDate { get; set; } public List<WorkflowReader> Readers {get; set; } } }
Models/WorkflowReader.cs
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Models { public class WorkflowReader { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } // 外部キー [ForeignKey("Workflow")] [Required] public int WorkflowId { get; set; } [Required] public string Name{ get; set; } public Workflow Workflow {get; set; } } }
Models/CodeFirstSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace Models { public class CodeFirstSampleContext: DbContext { public CodeFirstSampleContext(DbContextOptions<CodeFirstSampleContext> options) : base(options) { } public DbSet<Workflow> Workflows { get; set; } public DbSet<WorkflowReader> WorkflowReaders { get; set; } } }
Startup.cs
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Models; using Workflow; namespace CodeFirstSample { public class Startup { private readonly IConfiguration configuration; public Startup(IHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", false, true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, true) .AddEnvironmentVariables(); configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddDbContext<ProofreadingWorkflowContext>(options => options.UseNpgsql(configuration["ConnectionStrings"])); services.AddScoped<IWorkflowService, WorkflowService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
最初のマイグレーションファイルを生成する
さてさて準備ができたところでマイグレーションファイルを生成しますよ。
dotnet tool update --global dotnet-ef --version 5.0.0-preview.7.20365.15
※.NET 5 で試す場合、バージョン指定しないと最新の安定板がインストールされ、コマンド実行時にエラーが出るので注意が必要です。
ドキュメントによると、「dotnet add package Microsoft.EntityFrameworkCore.Design」で「Microsoft.EntityFrameworkCore.Design」をインストール、ということなのですが、これでインストールされるのは安定板 (3.1.6) なので、 NuGet でインストールする必要があります。
(VSCode の NuGet Package Manager 便利ですね)
前置きが長くなりましたが、マイグレーションファイルを作成します。
dotnet ef migrations add InitialCreate
コマンドを実行すると、 Migrations というフォルダ以下に下記が出力されます。
- 20200719144103_InitialCreate.cs
- 20200719144103_InitialCreate.Designer.cs
- WorkflowSampleContextModelSnapshot.cs
コマンドを実行してデータベースに反映します。
dotnet ef database update
すでに同名のテーブルが存在する場合
普通にエラーになります。
ということで、 Code first の場合はデータベースを空で作成する必要があります。
(それが難しい場合は後述の DB first で進めるのが良さそうです)
マイグレーションの取り消し
データベースに反映したマイグレーションを取り消したい場合。
例えば上記のあと何らかのマイグレーションを実行し、それを取り消したい場合は下記のようにします。
dotnet ef database update InitialCreate
直前の処理をなかったことにする、というよりは、戻したい状態を指定してそこに戻る、という方法はちょっと Git を思い出させます。
ただ、特にデータを挿入する前にテーブル名間違えたりしていた場合は、マイグレーションファイルを作って~とするより、マイグレーションファイル、テーブルを削除 -> 作り直して再度生成、とした方が楽かもしれませんね。
デフォルト値の設定
ここまでを実行すると、とりあえずテーブルは生成されます。
が、データを追加しようとすると、「Workflow」テーブルで「LastUpdateDate」が Null だと怒られます。
というのも、「DatabaseGenerated」がつけられている要素は Entity Framework が生成する INSERT 文から外されるから + デフォルト値が設定されていないから です。
INSERT INTO "Workflow" ("CirculationLimit", "CreateUserMail", "ModelName", "ProductId", "SizaiCode") VALUES (@p4285, @p4286, @p4287, @p4288, @p4289) RETURNING "Id", "LastUpdateDate";
なお「Id」は(おそらく) データベースでは「serial」型として扱われるため、自動でインクリメントされた値が挿入されます。
最初「DatabaseGenerated」をつければ自動で値をセットしてくれるのかな?と思い込んでいたため、ちょっとハマりました。
デフォルト値の設定は Context クラスでできます。
Models/CodeFirstSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace Models { public class CodeFirstSampleContext: DbContext { ... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Workflow>() .Property(w => w.LastUpdateDate) .HasDefaultValueSql("CURRENT_TIMESTAMP"); } ... } }
現在時刻の設定を、ついつい DateTime.Now とかしたくなりますが、その場合「マイグレーションファイルを作った時間」がデフォルト値になってしまうので要注意です。
DB first
では、先ほど作ったデータベース、テーブルを使って DB first も試してみます。
dotnet new empty -n DbFiirstSample
NuGet で Code first と同じパッケージを入れておきます。
で、コマンドを実行します。
dotnet ef dbcontext scaffold "Host=localhost;Port=5432;Database=WorkflowSample;Username=postgres;Password=XXX" Npgsql.EntityFrameworkCore.PostgreSQL -d -o Models -n Models
- Getting Started | Npgsql Documentation
- EF Core tools reference (.NET CLI) - EF Core | Microsoft Docs
コマンドのオプションについて
DB first でコードを生成するコマンドのオプションはいくつかありますが、その中でも下記 3 つは特に必要かな~、と思いました。
- -d(--data-annotations): 生成するモデルクラスの要素にアノテーション([Key]とか)を付けます。
- -o(--output-dir): デフォルトだとプロジェクト直下にファイルが生成されるので、これを「Models」というフォルダー以下に生成されるよう変更しています。
- -n(--namespace): 生成されるクラスの namespace を指定しています。デフォルトでは「DbFiirstSample.Models」となります。
生成されたクラス
Models/Workflow.cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; namespace Models { public partial class Workflow { public Workflow() { WorkflowReaders = new HashSet<WorkflowReaders>(); } [Key] public int Id { get; set; } public int ProductId { get; set; } [Required] public string CreateUserMail { get; set; } [Column(TypeName = "timestamp with time zone")] public DateTime? CirculationLimit { get; set; } [Column(TypeName = "timestamp with time zone")] public DateTime LastUpdateDate { get; set; } [InverseProperty("Workflow")] public virtual ICollection<WorkflowReader> WorkflowReaders { get; set; } } }
Models/WorkflowReaders.cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; namespace Models { [Index(nameof(WorkflowId))] public partial class WorkflowReaders { [Key] public int Id { get; set; } public int WorkflowId { get; set; } [Required] public string Name{ get; set; } [ForeignKey(nameof(WorkflowId))] [InverseProperty("WorkflowReaders")] public virtual Workflow Workflow { get; set; } } }
Models/WorkflowSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace Models { public partial class WorkflowSampleContext : DbContext { public WorkflowSampleContext() { } public WorkflowSampleContext(DbContextOptions<WorkflowSampleContext> options) : base(options) { } public virtual DbSet<WorkflowReaders> WorkflowReaders { get; set; } public virtual DbSet<Workflow> Workflow { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=WorkflowSample;Username=postgres;Password=XXX"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); } }
Code first で生成されるコードとはやはり異なっていますね。
特に「DatabaseGenerated」については反映されていないようです。
(別のオプション、もしくは別のファイルで表現されている?)
【RxJS】処理に失敗したときに早期リターンしたい話
はじめに
C# でコードを書く時に、下記のような処理をよく書きます。
private async Task ExecuteExampleAsync() { var result1 = await DoSomethingAsync1(); if (result1 == null) { return; } var result2 = await DoSomethingAsync2(); if (result2 == null) { return; } return await DoSomethingAsync3(); }
例えば一つ目の処理に失敗した場合はそこで処理をストップする、という話なのですが、これを RxJS でやるとどうなるの?というのが前から気になっていたので試してみました。
Environments
- Angular ver.10.1.0-next.1
- RxJS ver.6.6
empty, throwError, throw new Error
今回調べてみたなかで、実現できそうな方法は3つありました。
サンプルコード(ベースだけ)
今回は下記の ngOnInit からメソッドを呼ぶ、という形で試すことにしました。
workflow-page.component.ts
import { Component, OnInit } from '@angular/core'; import { Observable, of, empty, throwError } from 'rxjs'; import { flatMap, catchError } from 'rxjs/operators'; @Component({ selector: 'app-workflow-page', templateUrl: './workflow-page.component.html', styleUrls: ['./workflow-page.component.css'] }) export class WorkflowPageComponent implements OnInit { constructor() { } ngOnInit(): void { // call methods } }
empty
「empty」が呼ばれると、subscribe の「complete」ハンドラーが呼ばれて処理が完了します。
... ngOnInit(): void { console.log('-- Throw empty from first call --'); this.executeEmpty(4); console.log('-- Throw empty from second call --'); this.executeEmpty(3); console.log('-- End --'); } private executeEmpty(startValue: number) { this.getEmpty(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getEmpty(result); }), catchError(error => { console.error(`catch: ${error}`); return of(error); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } private getEmpty(lastValue: number): Observable<number> { if (lastValue > 3) { return empty(); } return of(lastValue + 1); }
結果
-- Throw empty from first call -- complete -- Throw empty from second call -- 2nd execution: 4 complete -- End --
以上から、「empty」を使う場合、処理の結果(今回は null が返ってきた)は呼び出し先である「getEmpty」がハンドリングする必要があります。
呼び出し元には「complete」ハンドラーが呼ばれたことしか返ってこないからですね。
throwError
... ngOnInit(): void { console.log('-- Throw throwError from first call --'); this.executeThrowError(4); console.log('-- Throw throwError from second call --'); this.executeThrowError(3); console.log('-- Throw throwError with catchError from first call --'); this.executeThrowErrorWithCatchError(4); console.log('-- Throw throwError with catchError from second call --'); this.executeThrowErrorWithCatchError(3); console.log('-- End --'); } private executeThrowError(startValue: number) { this.getThrowError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowError(result); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } private executeThrowErrorWithCatchError(startValue: number) { this.getThrowError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowError(result); }), catchError(error => { console.error(`catch: ${error}`); return of(error); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } public getThrowError(lastValue: number): Observable<number> { if (lastValue > 3) { return throwError('Error from throwError'); } return of(lastValue + 1); }
結果
-- Throw throwError from first call -- error: Error from throwError ... -- Throw throwError from second call -- 2nd execution: 4 error: Error from throwError ... -- Throw throwError with catchError from first call -- catch: Error from throwError ... next: Error from throwError complete -- Throw throwError with catchError from second call -- 2nd execution: 4 catch: Error from throwError ... next: Error from throwError complete -- End --
「catchError」を使っていない場合はsubscribeの「error」ハンドラーが呼ばれます。
「catchError」を使っている場合、「catchError」で「of(error)」を返すと通常と同じく「next」と「complete」が呼ばれます。
ということで、処理に失敗したことを subscribe でエラーとしてハンドリングしたい場合は「catchError」無しで「throwError」を使うか、「catchError」からさらに「throwError」を実行する、といった書き方になりそうです。
throw new Error
「throw new Error」で Error を投げる、または呼び出したメソッド内で Error が発生した場合はどうでしょうか。
... ngOnInit(): void { console.log('-- Throw new Error from first call --'); this.executeThrowNewError(4); console.log('-- Throw new Error from second call --'); this.executeThrowNewError(3); } private executeThrowNewError(startValue: number) { this.getThrowNewError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowNewError(result); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } // throw new Error public getThrowNewError(lastValue: number): Observable<number> { if (lastValue > 3) { throw new Error('Error from new Error()'); } return of(lastValue + 1); }
結果
-- Throw new Error from first call -- ERROR Error: Error from new Error()
最初の結果しか出力されない。。。(´・ω・`)
というのも、 pipe でつながれていない、最初の処理で Error が発生してしまうと「catchError」や subscribe の「error」では受け取ることができず、処理が止まってしまうからですね。
引数を2回目の実行時に Error を投げることになる 3 に変更すると、下記のような結果になります。
-- Throw new Error from first call -- 2nd execution: 4 error: Error: Error from new Error() ...
ということで、 Observable を返すメソッド(関数)では「throw new Error」は使わない方が良さそうです。
また、メソッド内で Error が発生する場合も呼び出し元には「throwError」として返す必要があるかと。
処理失敗の通知に「throwError」を使うべきか
今回試した「empty」、「throwError」、「throw new Error」の中では「throwError」を使うのがよさそうです。
が、まだ処理失敗の通知に「throwError」を使うべきか、というのはよくわかっておりません。
もう少しサンプルなど漁った方がよさそうですね。
TypeOrm を追加する
はじめに
今回は TypeOrm を追加して Database にアクセスできるようにしますよ。
Angular 10 + NgRx + Nest.js でプロジェクトを作る
TypeOrm を使う
Install
前回と同じくいくつかパッケージをインストールします。
Nest.js のプロジェクトに統合するためのパッケージもあるので合わせてインストールします。
npm install --save @nestjs/typeorm typeorm pg
Configuration
前回は「ormconfig.json」を使って TypeOrm の設定( Database の接続文字列など)を行っていました。
Nest.js のプロジェクトでは、「app.module.ts」の中で設定することもできます。
server/app.module.ts
import { Module } from '@nestjs/common';
import { AngularUniversalModule } from '@nestjs/ng-universal';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { Workflow } from './entities/workflow.entity';
@Module({
imports: [
AngularUniversalModule.forRoot({
viewsPath: join(process.cwd(), 'dist/browser'),
bundle: require('../server/main'),
liveReload: false
}),
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "test",
password: "test",
database: "Example",
entities: [Workflow],
synchronize: true,
logging: true,
cache: false
})
],
controllers: [],
providers: []
})
export class ApplicationModule {}
結局 JSON ファイルを選択した
結局、前回同様「ormconfig.json」を使うことにしました。
理由は、マイグレーションファイルを使った Database 更新をしたい場合、 TypeOrm のコマンドを使うのですが、この時コマンドからは「app.module.ts」の内容が参照できません(多分)。
ということで、いずれにしても「ormconfig.json」が必要になるわけですね。
ormconfig.json
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "test",
"password": "test",
"database": "Example",
"entities": ["dist/**/entities/*.entity{.ts,.js}"],
"migrations": ["server/migrations/*.{.ts,.js}"],
"synchronize": true,
"logging": true,
"cache": false,
"cli": {
"entitiesDir": "server/entities",
"migrationsDir": "server/migrations"
}
}
なお「ormconfig.json」を使う場合の「app.module.ts」はこんな感じです。
server/app.module.ts
~省略~
@Module({
imports: [
~省略~
TypeOrmModule.forRoot()
],
controllers: [],
providers: []
})
export class ApplicationModule {}
Entity クラスを追加する
server/entities/workflow.entity.ts
import {Entity, PrimaryGeneratedColumn, Column, OneToMany, UpdateDateColumn} from "typeorm";
@Entity("Workflow")
export class Workflow {
@PrimaryGeneratedColumn()
id: number = -1;
@Column({ type: 'text', nullable: false })
createUserMail: string = '';
@Column({ type: 'timestamp with time zone', nullable: true })
circulationLimit: Date|null = null;
@Column({ type: 'timestamp with time zone', nullable: false })
lastUpdateDate: Date = new Date();
public create(createUserMail: string, circulationLimit: Date|null) {
if (createUserMail == null ||
createUserMail.length <= 0) {
throw new Error("Can not set createUserMail empty");
}
this.createUserMail = createUserMail;
this.update(circulationLimit);
}
public update(circulationLimit: Date|null) {
this.circulationLimit = circulationLimit;
this.lastUpdateDate = new Date();
}
}
「nest generate」を使う
TypeOrm のクラスにアクセスするため、いくつか Nest.js のプロジェクトにクラスを追加していきます。
が、「nest generate ~」を実行すると、ファイルが「src」ディレクトリにできてしまう。。。(´・ω・`)
サーバー側のファイルを置いている「server」ディレクトリ以下にファイルを出力するにはどうすれば・・・?
結果としては、プロジェクト直下に「nest-cli.json」を置くことで解決しました。
nest-cli.json
{
"collection": "@nestjs/schematics",
"sourceRoot": "server"
}
「@nestjs/testing」の追加
Controller や Service などのクラスを追加したところ、エラー発生。
どうも一緒に追加されたテストクラスが必要としている、「@nestjs/testing」が足りない模様。
ということでインストールしました。
npm install --save-dev @nestjs/testing
Repository クラスから Database にアクセスする
Database にアクセスする(失敗)
まず Database にアクセスするための Controller や Service を追加します。
server/workflow/workflow.controller.ts
import { Controller, Get } from '@nestjs/common';
import { WorkflowService } from './workflow.service';
import { Workflow } from '../entities/workflow.entity';
@Controller('workflow')
export class WorkflowController {
constructor(private readonly service: WorkflowService) {
}
@Get("search")
public async getWorkflowItems(): Promise<Workflow[]> {
return await this.service.findAll();
}
}
server/workflow/workflow.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Workflow } from '../entities/workflow.entity';
import { Repository } from 'typeorm';
import { of } from 'rxjs';
@Injectable()
export class WorkflowService {
constructor(
@InjectRepository(Workflow)
private workflowRepositiory: Repository<Workflow>
){}
public findAll(): Promise<Workflow[]> {
return this.workflowRepositiory.find();
}
}
server/app.module.ts
...
import { WorkflowController } from './workflow/workflow.controller';
import { WorkflowService } from './workflow/workflow.service';
import { Workflow } from './entities/workflow.entity';
@Module({
imports: [
...
],
controllers: [WorkflowController],
providers: [WorkflowService]
})
export class ApplicationModule {}
OK。では実行してみます。
…エラーが発生しました(´・ω・`)
... [Nest] 17376 - 07/11/2020, 8:22:27 PM [ExceptionHandler] Nest can't resolve dependencies of the WorkflowService (?). Please make sure that the argument WorkflowRepository at index [0] is available in the ApplicationModule context. Potential solutions: - If WorkflowRepository is a provider, is it part of the current ApplicationModule? - If WorkflowRepository is exported from a separate @Module, is that module imported within ApplicationModule? @Module({ imports: [ /* the Module containing WorkflowRepository */ ] }) +570ms Error: Nest can't resolve dependencies of the WorkflowService (?). Please make sure that the argument WorkflowRepository at index [0] is available in the ApplicationModule context. Potential solutions: - If WorkflowRepository is a provider, is it part of the current ApplicationModule? - If WorkflowRepository is exported from a separate @Module, is that module imported within ApplicationModule? @Module({ imports: [ /* the Module containing WorkflowRepository */ ] }) at Injector.lookupComponentInParentModules (C:\Users\example\Documents\workspace\proofreading-workflow\node_modules\@nestjs\core\injector\injector.js:190:19) at processTicksAndRejections (internal/process/task_queues.js:97:5) [nodemon] app crashed - waiting for file changes before starting...
依存を解決する
どうも Repository クラスが DI できてないようです。
結局「app.module.ts」にモジュールを追加しました。
server/app.module.ts
...
import { Workflow } from './entities/workflow.entity';
@Module({
imports: [
...
TypeOrmModule.forRoot(),
TypeOrmModule.forFeature([Workflow])
],
controllers: [ProofreaderController, WorkflowController],
providers: [ProofreaderService, WorkflowService]
})
export class ApplicationModule {}
ようやく Database にアクセスできるようになりました :).
参照
Angular 10 + NgRx + Nest.js でプロジェクトを作る
はじめに
今回はサーバー側のプログラムも追加してみます。
今回は TypeScript 好きである、ということもあって Nest.js を選びました。
Environments
- Angular ver.10.0.2
- @ngrx/store ver.9.2.0
- @ngrx/effects ver.9.2.0
- @nestjs/ng-universal ver.2.0.1
Angular ver.10のプロジェクトに @ngrx/store が追加できない
前回同様「npx ng add @ngrx/store」で NgRx を追加しようとするとエラーが発生。
Installing packages for tooling via npm. Installed packages for tooling via npm. The package that you are trying to add does not support schematics. You can try using a different version of the package or contact the package author to add ng-add support.
下記の Issue によると、「@latest」をつける必要があるとのこと。
npx ng add @ngrx/store@latest
「@latest」をつけない場合、 1.2.0 のような古いバージョンのものがインストールされているようでした。
cannot use ng add to install ngrx store · Issue #2604 · ngrx/platform · GitHub
「@ngrx/effects」については「@latest」が必要かどうかはわかりませんでしたが、とりあえずつけてインストールしました。
npx ng add @ngrx/effects@latest
Nest.js のプロジェクトを追加する
「@nestjs/ng-universal」を使って Nest.js のプロジェクトを追加します。
GitHub - nestjs/ng-universal: Angular Universal module for Nest framework (node.js) 🌷
元々は Server-Side Rendering のためのものではありますが。
「ng add」コマンドを実行すると、プロジェクト名を聞かれるので先に作成した Angular のプロジェクト名を入力します。
npx ng add @nestjs/ng-universal
プロジェクトを実行する(失敗)
「npm run serve」で実行し、「localhost:4200」にアクセスするわけですが、ここでエラーが発生しました。
... [Nest] 12736 - 07/08/2020, 1:04:45 AM [ExceptionsHandler] Cannot read property 'indexOf' of undefined +28479ms TypeError: Cannot read property 'indexOf' of undefined ...
下記の Issue によると、「liveReload」を false にする必要があるとのこと。
TypeError: Cannot read property 'indexOf' of undefined · Issue #188 · nestjs/ng-universal · GitHub
server/app.module.ts
import { Module } from '@nestjs/common';
import { AngularUniversalModule } from '@nestjs/ng-universal';
import { join } from 'path';
@Module({
imports: [
AngularUniversalModule.forRoot({
viewsPath: join(process.cwd(), 'dist/browser'),
bundle: require('../server/main'),
liveReload: false
})
]
})
export class ApplicationModule {}
白いページが表示される
「liveReload」自体も気になるところですが、とにあれこれでエラーなしでページを表示できました。
が、そこにあるのはただ真っ白のページ。Angular のページではありません。
プロジェクトの README.md にある「bootstrap」は(恐らく)仕様変更のため「AngularUniversalOptions」に存在せず、サンプルコードをそのままコピペすることはできないようです。
ただ、「localhost:4200/index.html」にアクセスすると、正しく Angular のページが表示できました。
ということで、 デフォルトの Static file が設定されていないようだ、ということはわかりました。
またまた README.md を見ると「templatePath」というプロパティがあったため、追加してみたのですが動作しないようです。
server/app.module.ts
import { Module } from '@nestjs/common';
import { AngularUniversalModule } from '@nestjs/ng-universal';
import { join } from 'path';
@Module({
imports: [
AngularUniversalModule.forRoot({
viewsPath: join(process.cwd(), 'dist/browser'),
bundle: require('../server/main'),
liveReload: false,
templatePath: join(process.cwd(), 'dist/browser/index.html'),
})
]
})
export class ApplicationModule {}
「@nestjs/serve-static」を使ったり、ルーティングで index.html を表示させる、といった処理が必要なのでしょうか。。。?
2020-07-08 追記
@nestjs/serve-static 追加
今回は Nest.js で静的ファイルを扱うことができるようにする「@nestjs/serve-static」を使って解決することにしました。
npm install --save @nestjs/serve-static
server/main.ts
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.setGlobalPrefix('api');
app.useStaticAssets(join(__dirname, '..', 'browser'));
await app.listen(4200);
}
bootstrap();