.NET8 Blazor Identity WebApp で複数のデータベース コンテキストを使用

# .NET8 Blazor Identity WebApp で複数の`DbContext`を使用
現在、複数のデータベースにアクセスする Blazor Web アプリケーションに取り組んでいます。
アプリケーションは 3 つのデータベースにアクセスします：
- 2 つは Postgresql データベースで、そのうち 1 つはメイン アプリケーションと Identity Server 認証の両方に使用されます
- 残りの 1 つは Oracle データベースです

複数のデータベースを使用するには、複数のデータベース コンテキスト オブジェクトを使用するだけです。
## 接続文字列
1. まず、`appsettings.json` ファイルで各データベースの接続文字列を定義しましょう
    ```json
    "ConnectionStrings": {
        "ApplicationConnection": "Host=127.0.0.1;Port=5432;Database=db1;Username=user1;Password=pasword1",
        "OracleConnection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.2)(PORT=1521))(CONNECT_DATA=(SERVER=dedicated)(SERVICE_NAME=service2)));User Id=user2;  Password=pasword2;",
        "AnotherConnection": "Host=127.0.0.3;Port=5432;Database=db3;Username=user3;Password=password3"
    },
    ```
## 設定
2. 次に、データベース コンテキスト ファクトリをサービスとして `Program.cs` ファイルに登録しましょう
    ```csharp
    var applicationConnectionString = builder.Configuration.GetConnectionString("ApplicationConnection") ?? throw new InvalidOperationException("Connection string 'ApplicationConnection' not found.");
    builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
        options.UseNpgsql(applicationConnectionString));
    var anotherConnectionString = builder.Configuration.GetConnectionString("AnotherConnection") ?? throw new InvalidOperationException("Connection string 'AnotherConnection' not found.");
    builder.Services.AddDbContextFactory<AnotherDbContext>(options =>
        options.UseNpgsql(anotherConnectionString));
    var oracleConnectionString = builder.Configuration.GetConnectionString("OracleConnection") ?? throw new InvalidOperationException("Connection string 'OracleConnection' not found.");
    builder.Services.AddDbContextFactory<OracleDbContext>(options =>
        options.UseOracle(oracleConnectionString));*/
    builder.Services.AddDatabaseDeveloperPageExceptionFilter();
    
    builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>() // 大事！ロールを使用するとき。
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddSignInManager()
        .AddDefaultTokenProviders();
    ```
3. まだ「Program.cs」ファイルで、アプリケーション データベースをシードしましょう
    ```csharp
    using (var scope = app.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
    
        var context = services.GetRequiredService<ApplicationDbContext>();
        context.Database.EnsureCreated();
        ApplicationDbInitializer.Initialize(context);
    }
    ```

## データベース コンテキスト
各データベースのデータベース コンテキスト オブジェクトを作成しましょう
### ApplicationDbContext
1. `Data\ApplicationDbContext.cs` で定義されている `ApplicationDbContext` は、`IdentityDbContext` クラスから継承して認証データも処理します：
    ```csharp
    using MyApp.Data.Models;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore;
    
    namespace MyApp.Data
    {
        public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
        {
            public DbSet<Datatype1> Data1 { get; set; }
            public DbSet<Datatype2> Data2 { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
    
                modelBuilder.Entity<Datatype1>().ToTable("Data1");
                modelBuilder.Entity<Datatype2>().ToTable("Data2");
            }

            public override void Dispose()
            {
                base.Dispose();
            }
    
            public override ValueTask DisposeAsync()
            {
                return base.DisposeAsync();
            }
        }
    }
    ```
    
### AnotherDbContext
1. `Data\AnotherDbContext.cs` で定義されている `AnotherDbContext` は、ただの `DbContext` クラスから継承します：
    ```csharp
    using MyApp.Data.Models;
    using Microsoft.EntityFrameworkCore;
    
    namespace MyApp.Data
    {
        public class AnotherDbContext(DbContextOptions<AnotherDbContext> options) : DbContext(options)
        {
            public DbSet<Datatype3> Data3 { get; set; }
        
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
    
                modelBuilder.Entity<Datatype3>().ToTable("Data3");
            }

            public override void Dispose()
            {
                base.Dispose();
            }
    
            public override ValueTask DisposeAsync()
            {
                return base.DisposeAsync();
            }
        }
    }
    ```
### OracleDbContext
1. `Data\OracleDbContext.cs` で定義されている `OracleDbContext` も、ただの `DbContext` クラスから継承します：
    ```csharp
    using MyApp.Data.Models;
    using Microsoft.EntityFrameworkCore;
    
    namespace MyApp.Data
    {
        public class OracleDbContext(DbContextOptions<OracleDbContext> options) : DbContext(options)
        {
            public DbSet<Datatype4> Data4 { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
    
                modelBuilder.Entity<Datatype4>().ToTable("Data4");
            }

            public override void Dispose()
            {
                base.Dispose();
            }
    
            public override ValueTask DisposeAsync()
            {
                return base.DisposeAsync();
            }
    
        }
    }
    ```
## マイグレーション
**パッケージ マネージャー コンソール**で：
1. プロジェクトのフォルダーを開きます
    ```
    cd .\DMSofuSakiTorokuSystem
    ```
2. アプリケーション データベース**のみ**の初期移行を作成します 
    ```
    Add-Migration Initial-Create -OutputDir Data\Migrations -Context ApplicationDbContext
    ```
3. アプリケーション データベース**のみ**を更新します
    ```
    Update-Database -Context ApplicationDbContext
    ```
※　他の 2 つのデータベースはアプリケーションによって管理されません
## シード

### DbInitializer
1. `Data\ApplicationDbInitializer.cs` ファイル内で、[データベースにデータをシードする](https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding)ために `Program.cs` の中で呼び出した `Initialize` メソッドを含む `ApplicationDbInitializer` クラスを作成します。データベース初期化子は、`Data\ApplicationDbContext.cs` で定義された 2 つのテーブルと、認証に関連し IdentityDbContext から継承された[テーブルをシード]()します：
    ```csharp
    using MyApp.Data.Models;
    using Microsoft.AspNetCore.Identity;
    using System.Globalization;
    
    namespace MyApp.Data
    {
        public class ApplicationDbInitializer
        {
            public static void Initialize(ApplicationDbContext context)
            {
                context.Database.EnsureCreated();
                if (context.Data1.Any() || context.Data2.Any() || context.Users.Any())
                {
                    // シードされました
                    return;
                }
                else
                {
                    // テーブル 1 ↓
                    IList<Datatype1> data1 = new List<Datatype1> {
                        new() { Id = 1, Name = "name1" },
                        new() { Id = 2, Name = "name2" }
                    };
                    context.Data1.AddRange(data1);

                    // テーブル 2 ↓
                    IList<Datatype2> data2 = new List<Datatype2> {
                        new() { Id = "1",  Value="value1"},
                        new() { Id = "2",  Value="value2"},
                    };
                    context.Data2.AddRange(data2);

                    // ↓ ↓ ↓ ↓ 認証のテーブル ↓ ↓ ↓ ↓
                    var hasher = new PasswordHasher<IdentityUser>();
                    IList<ApplicationUser> users = new List<ApplicationUser> {
                        new() {
                            Id = "user-id-01",
                            UserName = "user1",
                            NormalizedUserName = "USER1",
                            Email = "user1@domain.co.jp",
                            NormalizedEmail = "USER1@DOMAIN.CO.JP",
                            EmailConfirmed = true,
                            PasswordHash = hasher.HashPassword(null, "password1"),
                            SecurityStamp = Guid.NewGuid().ToString("D")
                        },
                        new() {
                            Id = "user-id-02",
                            UserName = "user2",
                            NormalizedUserName = "USER2",
                            Email = "user2@domain.co.jp",
                            NormalizedEmail = "USER2@DOMAIN.CO.JP",
                            EmailConfirmed = true,
                            PasswordHash = hasher.HashPassword(null, "password2"),
                            SecurityStamp = Guid.NewGuid().ToString("D")
                        }
                    };
                    context.Users.AddRange(users);
                    IList<IdentityRole> roles = new List<IdentityRole> {
                        new() {
                            Id="Admin",
                            Name="Admin"
                        },
                        new() {
                            Id="User",
                            Name="User"
                        },
                    };
                    context.Roles.AddRange(roles);
                    IList<IdentityUserRole<string>> userRoles = new List<IdentityUserRole<string>> {
                        new() {
                            UserId="user-id-01",
                            RoleId="Admin"
                        },
                        new() {
                            UserId="user-id-02",
                            RoleId="User"
                        }
                    };
                    context.UserRoles.AddRange(userRoles);
                }
                context.SaveChanges();
            }
        }
    }
    ```
## 注意点
データベース コンテキストとイニシャライザーの実装中に 2 つの問題が発生しました。
- 移行の作成時に、データベース テーブルに一部の列が作成されません。
- データベースは、**UTC** ではないため、`Parse()` または `ParseExact()` で文字列を解析することによって作成された `DateTime` 値を受け入れませんでした。
### フィールド対プロパティ
1. `{ get; set; }` の代わりに `=` を使用してデフォルト値で `Name` パブリック プロパティを初期化すると、[プロパティではなくフィールドを宣言することになります](https://learn.microsoft.com/en-us/ef/core/modeling/entity-properties?tabs=data-annotations%2Cwithout-nrt#included-and-excluded-properties)。**EF Core** がデータベース スキーマを構築するときにフィールドではなくプロパティを検索するため、**EF Core** はモデルにこれらのプロパティを含めない可能性があることを意味します。データベースのテーブルで「**Name**」カラムが作成されません。
    ```csharp
    using Microsoft.EntityFrameworkCore;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    
    namespace MyApp.Data.Models
    {
        [Table("Data1"), PrimaryKey("Id")]
        public class PersonalDataRequest
        {
            [Key]                    // このカラムが作成されます。
            public int Id { get; set; } = -1;
            [NotMapped]
            public string? IdString  // このカラムは作成されないように指示されてます。
            {
                get => Id == -1 ? "" : Id.ToString();
                set { }
            }
            [Required]
            public string Name = ""; // このカラムが作成されません。
        }
    }
    ```
2. 「**Name**」パブリック プロパティ宣言に `{ get; set; }` を追加すると、**EF Core** は同じ名前の列をデータベース内のテーブルに追加します：
    ```csharp
    [Required]
    public string Name { get; set; } = ""; // このカラムが作成されす！
    ```
### DateTime
1. データベースは、`Parse()` または `ParseExact()` で文字列を解析して作成された `DateTime` 値を受け入れません。これは、これらが **UTC** ではないためです。 `DateTime`が **UTC** にするには、`DateTime` で `ToUniversalTime()` メソッドを呼び出す必要があります：
    ```csharp
    string format = "yyyy/MM/dd HH:mm:ssK";
    CultureInfo provider = CultureInfo.InvariantCulture;
    Datatype dataItem = new() {
        // ...
        CreatetAt=DateTime.ParseExact("2024/04/20 09:00:00+00:00", format, provider).ToUniversalTime();
    }
    ```
## 参考
- [エンティティのプロパティ](https://learn.microsoft.com/ja-jp/ef/core/modeling/entity-properties?tabs=data-annotations%2Cwithout-nrt#included-and-excluded-properties)
- [複数のプロバイダーを使用した移行](https://learn.microsoft.com/ja-jp/ef/core/managing-schemas/migrations/providers?tabs=vs)
- [Entity Framework Core (EF Core) のシード データ](https://dotnettutorials.net/lesson/seed-data-in-entity-framework-core/)
- [データのシード処理](https://learn.microsoft.com/ja-jp/ef/core/modeling/data-seeding)