using Microsoft.Extensions.Logging; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using MiGu.DB.Abstractions.Modules; using MiGu.DB.Abstractions.Persistence; using MiGu.DB.Abstractions.Providers; using MiGu.DB.Abstractions.Runtime; using MiGu.DB.Kernel.Context; using MiGu.DB.Kernel.Conventions; using MiGu.DB.Kernel.Providers; using MiGu.DB.Kernel.Repositories; using MiGu.DB.Domains.Migrators; namespace MiGu.DB.Kernel.Hosting; public static class ServiceCollectionExtensions { public static IServiceCollection AddMiGuDb( this IServiceCollection services, IConfiguration configuration, Action? configure = null) { var options = new MiGuDbOptions { Provider = configuration["Database:Provider"] ?? "sqlite", ConnectionString = configuration.GetConnectionString("Platform") ?? configuration.GetConnectionString(ConnectionKey(configuration["Database:Provider"] ?? "sqlite")) ?? "", SchemaMode = ParseSchemaMode(configuration["Database:SchemaMode"]), ApplyDataMigratorsOnStartup = configuration.GetValue("Database:ApplyDataMigratorsOnStartup", true) }; configure?.Invoke(options); services.AddSingleton(options); services.TryAddSingleton(); services.TryAddScoped(sp => sp.GetRequiredService().Current); services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.AddDbContext((sp, builder) => { var opt = sp.GetRequiredService(); if (string.IsNullOrWhiteSpace(opt.ContentRootPath)) { var env = sp.GetService(); opt.ContentRootPath = env?.ContentRootPath ?? AppContext.BaseDirectory; } var providerName = NormalizeProviderName(opt.Provider); var setup = sp.GetServices() .FirstOrDefault(p => string.Equals(p.Name, providerName, StringComparison.OrdinalIgnoreCase)) ?? throw new InvalidOperationException($"未知数据库 Provider: {opt.Provider}"); setup.Configure(builder, ResolveConnectionString(opt, providerName)); builder.AddInterceptors(sp.GetRequiredService()); }); services.AddScoped(); services.AddScoped(typeof(IRepository<,>), typeof(Repository<,>)); services.AddScoped(typeof(IEditableRepository<>), typeof(EditableRepository<>)); services.AddScoped(typeof(IHistoryRepository<>), typeof(HistoryRepository<>)); services.AddMiGuDataMigrators(); return services; } public static IServiceCollection AddMiGuEntityModule(this IServiceCollection services) where TModule : class, IEntityModule, new() { services.TryAddEnumerable(ServiceDescriptor.Singleton()); new TModule().RegisterServices(services); return services; } private static string NormalizeProviderName(string provider) => provider.Trim().ToLowerInvariant() switch { "postgres" or "postgresql" => "npgsql", "mssql" => "sqlserver", var x => x }; private static MiGuSchemaMode ParseSchemaMode(string? value) { if (string.IsNullOrWhiteSpace(value)) return MiGuSchemaMode.Migrate; if (Enum.TryParse(value.Trim(), ignoreCase: true, out var mode)) return mode; // 兼容简写 return value.Trim().ToLowerInvariant() switch { "ensure" or "created" or "ensure-created" => MiGuSchemaMode.EnsureCreated, "migrations" or "ef" => MiGuSchemaMode.Migrate, _ => throw new InvalidOperationException( $"未知 Database:SchemaMode '{value}',允许值:Migrate、EnsureCreated") }; } private static string ConnectionKey(string provider) => NormalizeProviderName(provider) switch { "npgsql" => "PostgreSQL", "sqlserver" => "SqlServer", "mysql" => "MySql", _ => "Sqlite" }; private static string ResolveConnectionString(MiGuDbOptions options, string providerName) { if (!string.IsNullOrWhiteSpace(options.ConnectionString)) { return providerName == "sqlite" ? NormalizeSqliteConnection(options.ConnectionString, options.ContentRootPath) : options.ConnectionString; } var dataDir = Path.Combine(options.ContentRootPath, "data"); Directory.CreateDirectory(dataDir); return $"Data Source={Path.Combine(dataDir, "platform.db")}"; } private static string NormalizeSqliteConnection(string connection, string contentRoot) { var builder = new SqliteConnectionStringBuilder(connection); if (string.IsNullOrWhiteSpace(builder.DataSource) || builder.DataSource is ":memory:") return connection; if (!Path.IsPathRooted(builder.DataSource)) builder.DataSource = Path.Combine(contentRoot, builder.DataSource); var dir = Path.GetDirectoryName(builder.DataSource); if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); return builder.ToString(); } } public static class DatabaseInitializer { /// /// 启动期数据库初始化:按 SchemaMode 建库/迁移,再可选执行 DataMigrator。 /// public static async Task MigrateMiGuDbAsync(this IServiceProvider services, CancellationToken ct = default) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetService()?.CreateLogger("MiGu.DB.DatabaseInitializer"); if (options.SchemaMode == MiGuSchemaMode.EnsureCreated) { logger?.LogWarning( "Database:SchemaMode=EnsureCreated:仅按当前模型建库,不应用 Migrations。" + "改实体后若结构未更新,请删除平台库文件后重启。发版请改用 Migrate。"); await db.Database.EnsureCreatedAsync(ct); } else { var applied = (await db.Database.GetAppliedMigrationsAsync(ct)).ToList(); var pending = (await db.Database.GetPendingMigrationsAsync(ct)).ToList(); if (applied.Count == 0 && pending.Count == 0) { logger?.LogWarning("程序集内无 Migration,回退 EnsureCreated。"); await db.Database.EnsureCreatedAsync(ct); } else { // 旧库(EnsureCreated)无历史表行时写入基线,再 Migrate await BaselineExistingDatabaseAsync(db, logger, ct); await db.Database.MigrateAsync(ct); } } if (!options.ApplyDataMigratorsOnStartup) return; foreach (var migrator in scope.ServiceProvider.GetServices().OrderBy(m => m.Order)) await migrator.MigrateAsync(db, ct); } /// /// 已有表结构但无 __EFMigrationsHistory 时:将全部 pending Migration 记为已应用。 /// 前提:库由 EnsureCreated 按「当前模型 tip」建成,与最新 Snapshot 一致;否则应删库后走 Migrate,或手工对齐。 /// private static async Task BaselineExistingDatabaseAsync( MiGuDbContext db, ILogger? logger, CancellationToken ct) { var applied = await db.Database.GetAppliedMigrationsAsync(ct); if (applied.Any()) return; var pending = (await db.Database.GetPendingMigrationsAsync(ct)).ToList(); if (pending.Count == 0) return; var creator = db.GetService(); if (!await creator.ExistsAsync(ct) || !await creator.HasTablesAsync(ct)) return; var history = db.GetService(); var productVersion = ProductInfo.GetEFCoreVersion(); logger?.LogWarning( "检测到已有表且无迁移历史,将 {Count} 个 pending Migration 全部写入基线(假定库结构已对齐模型 tip):{Ids}", pending.Count, string.Join(", ", pending)); foreach (var migrationId in pending) { var sql = history.GetInsertScript(new HistoryRow(migrationId, productVersion)); await db.Database.ExecuteSqlRawAsync(sql, ct); } } } internal static class ProductInfo { public static string GetEFCoreVersion() { var asm = typeof(DbContext).Assembly.GetName().Version; return asm?.ToString(3) ?? "8.0.10"; } }