.Net6 DateOnly型でテーブルをつくる

2年ぶりの記事を書いています。
コロナ禍で環境が大幅に変わったりと色々大変でして, 書く時間がなくて...


#　はじめに
最近はMicrosoftがすごいと噂を聞きまして,
自分の新サービスをBlazorで書いてみようかとおもってはじめたのですが
結構苦労したのでここに自分用のメモとして残しておきます.

環境は

```
C#
.Net6
EFCore6
Blazor
SQLServer
```


# 事象
EFCore6 から *DateOnly* 型が導入されたようなのですが


```:/Shared/Models/Building.cs
using System.ComponentModel.DataAnnotations;

namespace BlazorApp.Shared.Models
{
    public class Building
    {
        public DateOnly BuildedDate { get; set; }
    }
}
```

上記のモデルで、migrateを実行すると

```
dotnet ef migrations add Building
```


エラーになって、Migrationファイルが作成されません。

```
The property 'Building.BuildedDate' could not be mapped because it is of type 'DateOnly',which is not a supported primitive type or a valid entity type. 
Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.
Ignore' in 'OnModelCreating'.

[日本語訳]
プロパティ 'Building.BuildedDate'は、サポートされているプリミティブ型または有効なエンティティ型ではないタイプ 'DateOnly'であるため、マップできませんでした。このプロパティを明示的にマップするか、「[NotMapped]」属性を使用するか、「OnModelCreating」の「EntityTypeBuilder.Ignore」を使用して無視します。
```

このエラーに従ってると目的が達成できなくなりますね....


# ひとまずDateTime型でMigrationファイルをつくる
```diff:/Shared/Models/Building.cs
- public DateOnly BuildedDate { get; set; }
+ public DateTime BuildedDate { get; set; }
```

```
dotnet ef migrations add Building
```
上記を実行すると

```:/Server/Migrations/2022XXXXX_Building.cs
namespace BlazorApp.Server.Migrations
{
    public partial class InitialTables : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Buildings",
                columns: table => new
                {
                    BuildingDate = table.Column<DateTime>(type: "datetime2", nullable: false),

   以下略
```

次に,  DatetimeをDateOnlyに変更

```diff:/Server/Migrations/2022XXXXX_Building.cs
- BuildingDate = table.Column<DateTime>(type: "datetime2", nullable: false),
+ BuildingDate = table.Column<DateOnly>(type: "date", nullable: false),
```
```diff:/Shared/Models/Building.cs
- public DateTime BuildedDate { get; set; }
+ public DateOnly BuildedDate { get; set; }
```

この段階でMigrateしても上でみたおなじエラーがでます。
そこで、以下のファイルを修正します

```diff:/Server/Data/AppDbContext.cs
using BlazorApp.Shared.Models;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.ChangeTracking;
+ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace BlazorApp.Server.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) : base(options)
        {
        }
+
+        protected override void ConfigureConventions(ModelConfigurationBuilder builder)
+        {
+            builder.Properties<DateOnly>()
+                .HaveConversion<DateOnlyConverter>()
+                .HaveColumnType("date");
+            builder.Properties<DateOnly?>()
+                .HaveConversion<NullableDateOnlyConverter,NullableDateOnlyComparer>()
+                .HaveColumnType("date");
+       }

        public DbSet<Building> Buildings => Set<Building>();
    }
+
+    /// <summary>
+    /// Converts <see cref="DateOnly" /> to <see cref="DateTime"/> and vice versa.
+    /// </summary>
+    public class DateOnlyConverter : ValueConverter<DateOnly, DateTime>
+    {
+        /// <summary>
+        /// Creates a new instance of this converter.
+        /// </summary>
+        public DateOnlyConverter() : base(
+                d => d.ToDateTime(TimeOnly.MinValue),
+                d => DateOnly.FromDateTime(d))
+        { }
+    }
+
+    /// <summary>
+    /// Compares <see cref="DateOnly" />.
+    /// </summary>
+    public class DateOnlyComparer : ValueComparer<DateOnly>
+    {
+        /// <summary>
+        /// Creates a new instance of this converter.
+        /// </summary>
+        public DateOnlyComparer() : base(
+            (d1, d2) => d1 == d2 && d1.DayNumber == d2.DayNumber,
+            d => d.GetHashCode())
+        {
+        }
+    }
+
+    /// <summary>
+    /// Converts <see cref="DateOnly?" /> to <see cref="DateTime?"/> and vice versa.
+    /// </summary>
+    public class NullableDateOnlyConverter : ValueConverter<DateOnly?, DateTime?>
+    {
+        /// <summary>
+        /// Creates a new instance of this converter.
+        /// </summary>
+        public NullableDateOnlyConverter() : base(
+            d => d == null
+                ? null
+                : new DateTime?(d.Value.ToDateTime(TimeOnly.MinValue)),
+            d => d == null
+                ? null
+                : new DateOnly?(DateOnly.FromDateTime(d.Value)))
+        { }
+    }
+
+    /// <summary>
+    /// Compares <see cref="DateOnly?" />.
+    /// </summary>
+    public class NullableDateOnlyComparer : ValueComparer<DateOnly?>
+    {
+        /// <summary>
+        /// Creates a new instance of this converter.
+        /// </summary>
+        public NullableDateOnlyComparer() : base(
+            (d1, d2) => d1 == d2 && d1.GetValueOrDefault().DayNumber == d2.GetValueOrDefault().DayNumber,
+            d => d.GetHashCode())
+        {
+        }
+    }
}
```


こちらを[参照](https://github.com/dotnet/efcore/issues/24507)しました

他にも、[以下のやり方](https://github.com/marcominerva/TinyHelpers/tree/master/src/TinyHelpers.EntityFrameworkCore)があるみたいです。
> Install-Package TinyHelpers.EntityFrameworkCore

このあと

```
dotnet ef migrations add AlterDateOnly
dotnet ef database update
```
これで, カラムがDateOnly型に変更できました
以下のファイルが追加されます


```:Service/Migrations/2022XXX_AlterDateOnly.cs
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace BlazorApp.Server.Migrations
{
    public partial class AlterDateOnly : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AlterColumn<DateTime>(
                name: "BuildedDate",
                table: "Buildings",
                type: "date",
                nullable: false,
                oldClrType: typeof(DateTime),
                oldType: "datetime2");
        }
    }
}
```



# うまくいってるんだけど....
以下のファイルをのぞいてみると

```:Server/Migrations/AppDbContextModelSnapshot.cs
using BlazorApp.Server.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace BlazorApp.Server.Migrations
{
    [DbContext(typeof(AppDbContext))]
    partial class AppDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity("ChintaiApp.Shared.Models.Building", b =>
                {
                    b.Property<DateTime>("BuildedDate")
                        .HasColumnType("date");
以下略
```

*DateTime* のままなんですよね... 
これでうまくいってるけど、なぜなんでしょうね。。。


# 最後に
日本語でこれに関する記事が見当たらなかったので,　記事を書きました.
Date型すら標準機能で作れない.Netってフレームワークの完成度が低い気がして....
DateOnly型がないときは, どうやって開発してたんですかね?
不思議だ

次は Seedについて書きます。