【ASP.NET Core】【EntityFramework Core】【C#】 .NET 5 から .NET 6 に更新する
はじめに
これは 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 にデータをインサートする、アップデートする際に発生するため、参照だけなら(それでも変更はしておいた方が良いと思いますが)そのままでも何とかなりそうです。