diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9f80faf..9755770 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.0", + "version": "10.0.7", "commands": [ "dotnet-ef" ], diff --git a/Avalonia-API/Avalonia-API.csproj b/Avalonia-API/Avalonia-API.csproj index 5d22a45..8bbb938 100644 --- a/Avalonia-API/Avalonia-API.csproj +++ b/Avalonia-API/Avalonia-API.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Avalonia-API/appsettings.json b/Avalonia-API/appsettings.json index 245c998..91235d1 100644 --- a/Avalonia-API/appsettings.json +++ b/Avalonia-API/appsettings.json @@ -14,8 +14,8 @@ "RefreshTokenDays": 30 }, "DatabaseConfiguration": { - "Provider": "SQLite", - "ConnectionString": "Data Source=avalonia-api.db", + "Provider": "MySQL", + "ConnectionString": "Server=127.0.0.1;Port=3306;Database=avalonia-api;Uid=root;Pwd=123456;Max Pool Size=100;Min Pool Size=5;AllowZeroDateTime=True;AllowLoadLocalInfile=true;SslMode=Required", "AutoMigrate": true, "RecreateDatabase": false, "EnableDetailedLog": false, diff --git a/Avalonia-EFCore/Avalonia-EFCore.csproj b/Avalonia-EFCore/Avalonia-EFCore.csproj index 6640c83..bc55af8 100644 --- a/Avalonia-EFCore/Avalonia-EFCore.csproj +++ b/Avalonia-EFCore/Avalonia-EFCore.csproj @@ -8,17 +8,17 @@ - - - - + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Avalonia-EFCore/Database/AppDataContextFactory.cs b/Avalonia-EFCore/Database/AppDataContextFactory.cs index 84caefa..114941f 100644 --- a/Avalonia-EFCore/Database/AppDataContextFactory.cs +++ b/Avalonia-EFCore/Database/AppDataContextFactory.cs @@ -13,9 +13,92 @@ namespace Avalonia_EFCore.Database /// 命令行参数。 /// 配置好的数据上下文实例。 public AppDataContext CreateDbContext(string[] args) + { + return new AppDataContext(DesignTimeDatabaseConfiguration.Create(args)); + } + } + + /// + /// SQLite 迁移设计时工厂。 + /// + public sealed class SqliteAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public SqliteAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SQLite)); + } + + /// + /// SQL Server 迁移设计时工厂。 + /// + public sealed class SqlServerAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public SqlServerAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SqlServer)); + } + + /// + /// PostgreSQL 迁移设计时工厂。 + /// + public sealed class PostgreSqlAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public PostgreSqlAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.PostgreSQL)); + } + + /// + /// MySQL 迁移设计时工厂。 + /// + public sealed class MySqlAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public MySqlAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL)); + } + + internal static class DesignTimeDatabaseConfiguration + { + public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite) { DatabaseProviderRegistry.RegisterDefaults(); - return new AppDataContext(DatabaseConfiguration.ForSQLite("avalonia-api.db")); + + var provider = GetProvider(args) ?? defaultProvider; + return provider switch + { + DatabaseProvider.SQLite => DatabaseConfiguration.ForSQLite("avalonia-api.db"), + DatabaseProvider.SqlServer => DatabaseConfiguration.ForSqlServer("(localdb)\\MSSQLLocalDB", "AvaloniaApi"), + DatabaseProvider.PostgreSQL => DatabaseConfiguration.ForPostgreSQL("localhost", "avalonia_api", "postgres", "postgres"), + DatabaseProvider.MySQL => DatabaseConfiguration.ForMySQL("localhost", "avalonia_api", "root", "root"), + _ => DatabaseConfiguration.ForSQLite("avalonia-api.db"), + }; + } + + private static DatabaseProvider? GetProvider(string[] args) + { + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + string? value = null; + + if (arg.Equals("--provider", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + value = args[i + 1]; + } + else if (arg.StartsWith("--provider=", StringComparison.OrdinalIgnoreCase)) + { + value = arg["--provider=".Length..]; + } + + if (!string.IsNullOrWhiteSpace(value) + && Enum.TryParse(value, ignoreCase: true, out var provider)) + { + return provider; + } + } + + return null; } } } diff --git a/Avalonia-EFCore/Database/DatabaseExtensions.cs b/Avalonia-EFCore/Database/DatabaseExtensions.cs index e67373a..c09bc61 100644 --- a/Avalonia-EFCore/Database/DatabaseExtensions.cs +++ b/Avalonia-EFCore/Database/DatabaseExtensions.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Avalonia_EFCore.Database @@ -19,6 +20,14 @@ namespace Avalonia_EFCore.Database // 注册配置 services.AddSingleton(config); + if (typeof(TContext) == typeof(AppDataContext)) + { + services.AddProviderAppDataContext(config); + services.AddScoped>(); + + return services; + } + // 注册 DbContext services.AddDbContext(options => { @@ -31,6 +40,31 @@ namespace Avalonia_EFCore.Database return services; } + private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config) + { + switch (config.Provider) + { + case DatabaseProvider.SQLite: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.SqlServer: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.PostgreSQL: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.MySQL: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + default: + throw new NotSupportedException($"数据库提供程序 {config.Provider} 未注册。"); + } + } + /// /// 初始化数据库(在应用启动时调用一次)。 /// diff --git a/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs b/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs index 7cd9bce..33e4477 100644 --- a/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs +++ b/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs @@ -52,7 +52,7 @@ namespace Avalonia_EFCore.Database opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); })); Register(DatabaseProvider.MySQL, (opts, cs, timeout) => - opts.UseMySql(cs, ServerVersion.AutoDetect(cs), o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); })); + opts.UseMySQL(cs, o => o.CommandTimeout(timeout))); } } } diff --git a/Avalonia-EFCore/Database/ProviderAppDataContexts.cs b/Avalonia-EFCore/Database/ProviderAppDataContexts.cs new file mode 100644 index 0000000..13bf6cd --- /dev/null +++ b/Avalonia-EFCore/Database/ProviderAppDataContexts.cs @@ -0,0 +1,30 @@ +namespace Avalonia_EFCore.Database +{ + /// + /// SQLite 专用 DbContext,用于隔离 SQLite 迁移集。 + /// + public sealed class SqliteAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// SQL Server 专用 DbContext,用于隔离 SQL Server 迁移集。 + /// + public sealed class SqlServerAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// PostgreSQL 专用 DbContext,用于隔离 PostgreSQL 迁移集。 + /// + public sealed class PostgreSqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// MySQL 专用 DbContext,用于隔离 MySQL 迁移集。 + /// + public sealed class MySqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..fd0442c --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,175 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260520082626_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..49f8086 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using MySql.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + userid = table.Column(name: "user-id", type: "int", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "varchar(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false), + expiresat = table.Column(name: "expires-at", type: "datetime(6)", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "datetime(6)", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "varchar(128)", maxLength: 128, nullable: true), + device = table.Column(type: "varchar(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "varchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "用户主键") + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + name = table.Column(type: "varchar(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "varchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "varchar(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "天气预报主键") + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "varchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..cca9450 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,181 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260520083306_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..6d9aca2 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "varchar(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..3ab0fa9 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs @@ -0,0 +1,178 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + partial class MySqlAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..4d16e82 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,184 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260520082617_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..507419f --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + userid = table.Column(name: "user-id", type: "integer", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "character varying(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false), + expiresat = table.Column(name: "expires-at", type: "timestamp with time zone", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "timestamp with time zone", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "character varying(128)", maxLength: 128, nullable: true), + device = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "character varying(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "integer", nullable: false, comment: "用户主键") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "character varying(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "character varying(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "integer", nullable: false, comment: "天气预报主键") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "integer", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "character varying(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..eb576d5 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260520083254_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..b0f17c3 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "character varying(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..9655547 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs @@ -0,0 +1,187 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + partial class PostgreSqlAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs similarity index 97% rename from Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs rename to Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs index 82082bf..1dacf6c 100644 --- a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs @@ -6,9 +6,9 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { - [DbContext(typeof(AppDataContext))] + [DbContext(typeof(SqliteAppDataContext))] [Migration("20260514000100_InitialCreate")] partial class InitialCreate { diff --git a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs similarity index 87% rename from Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs rename to Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs index 0915dc1..589d107 100644 --- a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs @@ -1,11 +1,10 @@ using System; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { /// /// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。 @@ -21,8 +20,7 @@ namespace Avalonia_EFCore.Migrations Id = table.Column(name: "id", nullable: false, comment: "用户主键") .Annotation("SqlServer:Identity", "1, 1") .Annotation("Sqlite:Autoincrement", true) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Name = table.Column(name: "name", maxLength: 100, nullable: true, comment: "用户名称"), Email = table.Column(name: "email", maxLength: 200, nullable: true, comment: "用户邮箱"), CreatedAt = table.Column(name: "created-at", nullable: false, comment: "创建时间"), @@ -41,8 +39,7 @@ namespace Avalonia_EFCore.Migrations Id = table.Column(name: "id", nullable: false, comment: "天气预报主键") .Annotation("SqlServer:Identity", "1, 1") .Annotation("Sqlite:Autoincrement", true) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Date = table.Column(name: "date", nullable: false, comment: "预报日期"), TemperatureC = table.Column(name: "temperature-c", nullable: false, comment: "摄氏温度"), Summary = table.Column(name: "summary", maxLength: 200, nullable: true, comment: "天气摘要"), diff --git a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs similarity index 97% rename from Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs rename to Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs index fa1adb9..3425bda 100644 --- a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs @@ -8,9 +8,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { - [DbContext(typeof(AppDataContext))] + [DbContext(typeof(SqliteAppDataContext))] [Migration("20260515072045_AutoMigration_20260515152037")] partial class AutoMigration_20260515152037 { diff --git a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs similarity index 94% rename from Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs rename to Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs index e211d68..42d50b7 100644 --- a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs @@ -2,7 +2,7 @@ #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { /// public partial class AutoMigration_20260515152037 : Migration diff --git a/Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs similarity index 98% rename from Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.Designer.cs rename to Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs index 389ebd2..2cf45c8 100644 --- a/Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.Designer.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs @@ -8,9 +8,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { - [DbContext(typeof(AppDataContext))] + [DbContext(typeof(SqliteAppDataContext))] [Migration("20260515085847_AutoMigration_20260515165835")] partial class AutoMigration_20260515165835 { diff --git a/Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.cs b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs similarity index 98% rename from Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.cs rename to Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs index 3678094..3388890 100644 --- a/Avalonia-EFCore/Migrations/20260515085847_AutoMigration_20260515165835.cs +++ b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { /// public partial class AutoMigration_20260515165835 : Migration diff --git a/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..13eb309 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,179 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260520083230_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..01ff29d --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "TEXT", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs similarity index 92% rename from Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs rename to Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs index 84f508d..6438beb 100644 --- a/Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs +++ b/Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs @@ -7,15 +7,15 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Avalonia_EFCore.Migrations +namespace Avalonia_EFCore.Migrations.SQLite { - [DbContext(typeof(AppDataContext))] - partial class AppDataContextModelSnapshot : ModelSnapshot + [DbContext(typeof(SqliteAppDataContext))] + partial class SqliteAppDataContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => { @@ -102,6 +102,12 @@ namespace Avalonia_EFCore.Migrations .HasColumnName("name") .HasComment("用户名称"); + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + b.Property("PhoneNumber") .HasMaxLength(50) .HasColumnType("TEXT") diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..cb50f7f --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,184 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260520082607_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..cb9d496 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs @@ -0,0 +1,96 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + userid = table.Column(name: "user-id", type: "int", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "nvarchar(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false), + expiresat = table.Column(name: "expires-at", type: "datetime2", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "datetime2", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "nvarchar(128)", maxLength: 128, nullable: true), + device = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "nvarchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "用户主键") + .Annotation("SqlServer:Identity", "1, 1"), + name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "nvarchar(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "天气预报主键") + .Annotation("SqlServer:Identity", "1, 1"), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..a594598 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260520083242_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..907c311 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..955a92c --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs @@ -0,0 +1,187 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + partial class SqlServerAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Models/UserEntity.cs b/Avalonia-EFCore/Models/UserEntity.cs index d6da474..89b2326 100644 --- a/Avalonia-EFCore/Models/UserEntity.cs +++ b/Avalonia-EFCore/Models/UserEntity.cs @@ -28,6 +28,14 @@ namespace Avalonia_EFCore.Models [MaxLength(100)] public string? Name { get; set; } + /// + /// 获取或设置用户密码哈希值。 + /// + [Comment("密码哈希值")] + [Column("password-hash")] + [MaxLength(200)] + public string? PasswordHash { get; set; } + /// /// 获取或设置用户邮箱。 /// diff --git a/Avalonia-PC/Avalonia-PC.csproj b/Avalonia-PC/Avalonia-PC.csproj index e596ba2..46bde58 100644 --- a/Avalonia-PC/Avalonia-PC.csproj +++ b/Avalonia-PC/Avalonia-PC.csproj @@ -36,7 +36,7 @@ - + diff --git a/Avalonia-Services/Avalonia-Services.csproj b/Avalonia-Services/Avalonia-Services.csproj index e087112..19a6a29 100644 --- a/Avalonia-Services/Avalonia-Services.csproj +++ b/Avalonia-Services/Avalonia-Services.csproj @@ -8,7 +8,7 @@ - + diff --git a/scripts/add-migration.ps1 b/scripts/add-migration.ps1 index 9fce894..2df4c3f 100644 --- a/scripts/add-migration.ps1 +++ b/scripts/add-migration.ps1 @@ -1,6 +1,7 @@ param( [string]$Name, - [string]$Context = "AppDataContext", + [ValidateSet("SQLite", "SqlServer", "PostgreSQL", "MySQL", "All")] + [string]$Provider = "All", [string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj", [string]$StartupProject = "Avalonia-API/Avalonia-API.csproj", [string]$OutputDir = "Migrations" @@ -21,45 +22,71 @@ if ($LASTEXITCODE -ne 0) { throw "dotnet tool restore failed." } -Write-Host "Generating migration '$Name'..." -dotnet tool run dotnet-ef migrations add $Name ` - --project $Project ` - --startup-project $StartupProject ` - --context $Context ` - --output-dir $OutputDir -if ($LASTEXITCODE -ne 0) { - throw "dotnet ef migrations add failed." +function Get-ContextName([string]$providerName) { + switch ($providerName) { + "SQLite" { return "SqliteAppDataContext" } + "SqlServer" { return "SqlServerAppDataContext" } + "PostgreSQL" { return "PostgreSqlAppDataContext" } + "MySQL" { return "MySqlAppDataContext" } + default { throw "Unsupported provider '$providerName'." } + } } -$migrationDir = Join-Path (Split-Path $Project -Parent) $OutputDir -$migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" | - Where-Object { $_.Name -notlike "*.Designer.cs" } | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 +function Add-ProviderMigration([string]$providerName) { + $context = Get-ContextName $providerName + $providerOutputDir = Join-Path $OutputDir $providerName -if ($null -eq $migrationFile) { - throw "Migration file was not found for '$Name'." -} - -$content = Get-Content $migrationFile.FullName -Raw -$upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") -$downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") - -$upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" } -$downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" } - -if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) { - Write-Host "No model changes were detected. Removing empty migration '$Name'..." - dotnet tool run dotnet-ef migrations remove --force ` + Write-Host "Generating migration '$Name' for $providerName..." + dotnet tool run dotnet-ef migrations add $Name ` --project $Project ` --startup-project $StartupProject ` - --context $Context + --context $context ` + --output-dir $providerOutputDir if ($LASTEXITCODE -ne 0) { - throw "dotnet ef migrations remove failed." + throw "dotnet ef migrations add failed for $providerName." } - exit 0 + + $migrationDir = Join-Path (Split-Path $Project -Parent) $providerOutputDir + $migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" | + Where-Object { $_.Name -notlike "*.Designer.cs" } | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + + if ($null -eq $migrationFile) { + throw "Migration file was not found for '$Name' ($providerName)." + } + + $content = Get-Content $migrationFile.FullName -Raw + $upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") + $downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") + + $upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" } + $downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" } + + if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) { + Write-Host "No model changes were detected for $providerName. Removing empty migration '$Name'..." + dotnet tool run dotnet-ef migrations remove --force ` + --project $Project ` + --startup-project $StartupProject ` + --context $context + if ($LASTEXITCODE -ne 0) { + throw "dotnet ef migrations remove failed for $providerName." + } + return + } + + Write-Host "Migration generated for ${providerName}:" + Write-Host " $($migrationFile.FullName)" } -Write-Host "Migration generated:" -Write-Host " $($migrationFile.FullName)" -Write-Host "Review the migration, then start the app. Startup will automatically apply pending migrations." +$providers = if ($Provider -eq "All") { + @("SQLite", "SqlServer", "PostgreSQL", "MySQL") +} else { + @($Provider) +} + +foreach ($providerName in $providers) { + Add-ProviderMigration $providerName +} + +Write-Host "Review the migration files, then start the app. Startup will apply the migration set matching DatabaseConfiguration.Provider."