はじめに
これは C# Advent Calendar 2021 カレンダー2の15日目の記事です。
祝 .NET 6 リリース👏 ということで、手元にあった ASP.NET Core + EntityFramework Core(DB は PostgreSQL) のプロジェクトを更新してみました。
環境(更新後)
- .NET ver.6.0.101
- NLog.Web.AspNetCore ver.4.14.0
- Npgsql.EntityFrameworkCore.PostgreSQL ver.6.0.1
- Microsoft.EntityFrameworkCore ver.6.0.1
- Microsoft.EntityFrameworkCore.Design ver.6.0.1
- Newtonsoft.Json ver.13.0.1
- Microsoft.AspNetCore.Mvc.NewtonsoftJson ver.6.0.1
- Microsoft.AspNetCore.Identity.EntityFrameworkCore ver.6.0.1
ASP.NET Core を更新する
必須の作業
global.json (ある場合) と .csproj のバージョンを更新するだけです。
ApprovementWorkflowSample.csproj
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.1"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1"/> </ItemGroup> </Project>
ついでに「ImplicitUsings」を有効にしておくと、「using System;」などを省略できるようになります。
.NET 6 っぽくする(任意)
ASP.NET Core ではこれまで Program.cs(メインクラス)、 Startup.cs(ミドルウェアの設定など) を中心に処理が書かれていました。 .NET 6 で新規プロジェクトを作ると、 C# 9 で導入された トップレベルステートメントが適用され、下記のような Program.cs が作成され、 Startup.cs の内容が統合されます。
Program.cs(新規プロジェクト)
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
これに倣って、 Program.cs に統合してみました。
Before
Program.cs
using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NLog.Web; namespace ApprovementWorkflowSample { public class Program { public static void Main(string[] args) { var logger = NLogBuilder.ConfigureNLog("Nlog.config").GetCurrentClassLogger(); try { CreateHostBuilder(args).Build().Run(); } catch (Exception ex) { logger.Error(ex, "Stopped program because of exception"); throw; } finally { NLog.LogManager.Shutdown(); } } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .ConfigureLogging((hostingContext, logging) => { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Trace); }) .UseNLog(); } }
Startup.cs
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ApprovementWorkflowSample.Applications; using ApprovementWorkflowSample.Models; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server; using ApprovementWorkflowSample.Approvements; namespace ApprovementWorkflowSample { public class Startup { private readonly IConfiguration configuration; public Startup(IConfiguration configuration) { this.configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddHttpClient(); services.AddControllers() .AddNewtonsoftJson(); services.AddDbContext<ApprovementWorkflowContext>(options => options.UseNpgsql(configuration["DbConnection"])); services.AddIdentity<ApplicationUser, IdentityRole<int>>() .AddUserStore<ApplicationUserStore>() .AddEntityFrameworkStores<ApprovementWorkflowContext>() .AddDefaultTokenProviders(); services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp => (ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>() ); services.AddScoped<IApplicationUsers, ApplicationUsers>(); services.AddScoped<IWorkflows, Workflows>(); services.AddScoped<IApplicationUserService, ApplicationUserService>(); services.AddScoped<IApprovementService, ApprovementService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapControllers(); }); } } }
After
Program.cs
using ApprovementWorkflowSample.Models; using ApprovementWorkflowSample.Applications; using ApprovementWorkflowSample.Approvements; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server; using NLog.Web; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; var logger = NLogBuilder.ConfigureNLog("Nlog.config").GetCurrentClassLogger(); try { var builder = WebApplication.CreateBuilder(args); builder.Host.ConfigureLogging(logging => { logging.ClearProviders(); logging.AddConsole(); }) .UseNLog(); builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddHttpClient(); builder.Services.AddControllers() .AddNewtonsoftJson(); builder.Services.AddDbContext<ApprovementWorkflowContext>(options => options.UseNpgsql(builder.Configuration["DbConnection"])); builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>() .AddUserStore<ApplicationUserStore>() .AddEntityFrameworkStores<ApprovementWorkflowContext>() .AddDefaultTokenProviders(); builder.Services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp => (ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>() ); builder.Services.AddScoped<IApplicationUsers, ApplicationUsers>(); builder.Services.AddScoped<IWorkflows, Workflows>(); builder.Services.AddScoped<IApplicationUserService, ApplicationUserService>(); builder.Services.AddScoped<IApprovementService, ApprovementService>(); // DI の設定など、 builder.Services への処理が終わったあとに実行する必要がある. var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapControllers(); }); app.Run(); } catch (Exception ex) { logger.Error(ex, "Stopped program because of exception"); } finally { NLog.LogManager.Shutdown(); }
- これまで DI で取得していた、「IConfiguration」などは、「WebApplicationBuilder」から取得できます。
- コマンドライン引数は「args」で取得できます。
- DI の設定など「builder.Services」に対する設定は、「builder.Build();」実行より前に行う必要があり、逆にすると実行時に例外が発生します。
コマンドライン引数など、暗黙の部分があったりする部分は気になるものの、全体を見るとシンプルになった気がします。
- エントリー ポイント - C# によるプログラミング入門 - ++C++; // 未確認飛行 C
- Announcing .NET 6 — The Fastest .NET Yet - .NET Blog
- What's new in ASP.NET Core 6.0 | Microsoft Docs
- Comparing WebApplicationBuilder to the Generic Host: Exploring .NET Core 6 - Part 2 | Andrew Lock
- Logging in .NET Core and ASP.NET Core | Microsoft Docs
EntityFramework Core を更新する
以前もそうだった気がしますが、更新の影響は EntityFramework Core の方が大きい気がします。
今回は Npgsql で日付型の扱いが変わった、というのが特に大きそうです。
ApplicationUser.cs
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNetCore.Identity; namespace ApprovementWorkflowSample.Applications { public class ApplicationUser: IdentityUser<int> { ... [Required] [Column("last_update_date", TypeName = "timestamp with time zone")] public DateTime LastUpdateDate { get; set; } ... public void Update(string userName, string? organization, string email, string password) { UserName = userName; Organization = organization; Email = email; // ハッシュ化されたパスワードを設定する. PasswordHash = new PasswordHasher<ApplicationUser>() .HashPassword(this, password); // このあと context.SaveChangesAsync() を実行すると例外発生 LastUpdateDate = DateTime.Now; } ... } }
例外
InvalidCastException: Cannot write DateTime with Kind=Local to PostgreSQL type 'timestamp with time zone', only UTC is supported. Note that it's not possible to mix DateTimes with different Kinds in an array/range. See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior.
TimeStamp はタイムゾーン有・無にかかわらず C# 側は DateTime で良いのですが、UT でなければ例外が発生します。
ApplicationUser.cs
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNetCore.Identity; namespace ApprovementWorkflowSample.Applications { public class ApplicationUser: IdentityUser<int> { ... [Required] [Column("last_update_date", TypeName = "timestamp with time zone")] public DateTime LastUpdateDate { get; set; } ... public void Update(string userName, string? organization, string email, string password) { ... // OK LastUpdateDate = DateTime.Now.ToUniversalTime(); } ... } }
今回更新したプロジェクトでは使っていませんでしたが、 PostgreSQL 側が 「date」 の場合は新たに追加された 「DateOnly」 型になる、といった点が主な変更となりました。
なお、上記の例外は DB にデータをインサートする、アップデートする際に発生するため、参照だけなら(それでも変更はしておいた方が良いと思いますが)そのままでも何とかなりそうです。