using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Prefab.Base; using Prefab.Data.Entities; namespace Prefab.Data.Seeder; /// /// Base infrastructure for executing database seeders with lookup synchronization support. /// /// The concrete seeder implementation. /// The database abstraction used by the seeder. public abstract class PrefabDbSeeder : ISeeder where TDb : IPrefabDb { /// public async Task Execute(IServiceProvider serviceProvider, CancellationToken cancellationToken) { try { var db = serviceProvider.GetRequiredService(); var options = serviceProvider.GetService>>()?.Value ?? new SeederOptions(); var seederName = typeof(TSeeder).FullName ?? typeof(TSeeder).Name; var runMode = options.RunMode; var shouldSeedData = false; await SeedLookups(db, cancellationToken); await ApplyViews(db, serviceProvider, cancellationToken); switch (runMode) { case RunMode.RunAlways: shouldSeedData = true; break; case RunMode.SingleRun: case RunMode.RunOnFirstLoadOnly: shouldSeedData = !db.SeederLogs.Any(x => x.SeederName == seederName); break; } if (!shouldSeedData) { return; } await SeedData(db, serviceProvider, cancellationToken); db.SeederLogs.Add(new SeederLog { SeederName = seederName, RunMode = runMode.ToString(), RunAt = DateTime.UtcNow }); await db.SaveChangesAsync(cancellationToken); } catch (Exception ex) { Console.WriteLine(ex); throw; } } /// /// Applies database views, ensuring dependent queries exist before data seeding runs. /// protected abstract Task ApplyViews(TDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken); /// /// Seeds data specific to the derived context. /// protected abstract Task SeedData(TDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken); private static async Task SeedLookups(TDb dbContext, CancellationToken cancellationToken) { var processedTypes = new HashSet(); var dbContextType = dbContext.GetType(); var dbSetProperties = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)); foreach (var prop in dbSetProperties) { var entityType = prop.PropertyType.GetGenericArguments()[0]; if (!processedTypes.Add(entityType)) { continue; } if (!IsSubclassOfGeneric(entityType, typeof(LookupEntity<>))) { continue; } var baseType = entityType.BaseType; if (baseType is not { IsGenericType: true } || baseType.GetGenericTypeDefinition() != typeof(LookupEntity<>)) { continue; } var enumType = baseType.GetGenericArguments()[0]; var method = typeof(PrefabDbSeeder).GetMethod(nameof(SeedLookupAsync), BindingFlags.NonPublic | BindingFlags.Static); if (method is null) { continue; } var genericMethod = method.MakeGenericMethod(entityType, enumType); await (Task)genericMethod.Invoke(null, new object[] { dbContext, cancellationToken })!; } return; static bool IsSubclassOfGeneric(Type type, Type generic) { while (type is not null && type != typeof(object)) { var current = type.IsGenericType ? type.GetGenericTypeDefinition() : type; if (current == generic) { return true; } type = type.BaseType!; } return false; } } private static async Task SeedLookupAsync(DbContext dbContext, CancellationToken cancellationToken) where TLookup : LookupEntity, new() where TEnum : PrefabEnum { var dbSet = dbContext.Set(); var enumMembers = PrefabEnum.List .Select(e => new { Value = e.Value, Name = e.Name, ShortName = GetOptionalString(e, "ShortName"), Description = GetOptionalString(e, "Description"), SortOrder = e.Value }) .ToList(); var existingRows = await dbSet.ToListAsync(cancellationToken); foreach (var member in enumMembers) { var lookupRow = existingRows.FirstOrDefault(r => r.Id == member.Value); if (lookupRow is null) { lookupRow = new TLookup { Id = member.Value, Name = member.Name, ShortName = member.ShortName, Description = member.Description, SortOrder = member.SortOrder }; dbSet.Add(lookupRow); } else if (lookupRow.Name != member.Name || lookupRow.ShortName != member.ShortName || lookupRow.Description != member.Description || lookupRow.SortOrder != member.SortOrder) { lookupRow.Name = member.Name; lookupRow.ShortName = member.ShortName; lookupRow.Description = member.Description; lookupRow.SortOrder = member.SortOrder; dbSet.Update(lookupRow); } } var validValues = enumMembers.Select(m => m.Value).ToHashSet(); foreach (var row in existingRows.Where(row => !validValues.Contains(row.Id))) { dbSet.Remove(row); } await dbContext.SaveChangesAsync(cancellationToken); static string? GetOptionalString(PrefabEnum value, string propertyName) { var property = value.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (property is null || property.PropertyType != typeof(string)) { return null; } return property.GetValue(value) as string; } } }