199 lines
7.0 KiB
C#
199 lines
7.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Base infrastructure for executing database seeders with lookup synchronization support.
|
|
/// </summary>
|
|
/// <typeparam name="TSeeder">The concrete seeder implementation.</typeparam>
|
|
/// <typeparam name="TDb">The database abstraction used by the seeder.</typeparam>
|
|
public abstract class PrefabDbSeeder<TSeeder, TDb> : ISeeder where TDb : IPrefabDb
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task Execute(IServiceProvider serviceProvider, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var db = serviceProvider.GetRequiredService<TDb>();
|
|
var options = serviceProvider.GetService<IOptions<SeederOptions<TSeeder>>>()?.Value ?? new SeederOptions<TSeeder>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies database views, ensuring dependent queries exist before data seeding runs.
|
|
/// </summary>
|
|
protected abstract Task ApplyViews(TDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Seeds data specific to the derived context.
|
|
/// </summary>
|
|
protected abstract Task SeedData(TDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken);
|
|
|
|
private static async Task SeedLookups(TDb dbContext, CancellationToken cancellationToken)
|
|
{
|
|
var processedTypes = new HashSet<Type>();
|
|
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<TSeeder, TDb>).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<TLookup, TEnum>(DbContext dbContext, CancellationToken cancellationToken)
|
|
where TLookup : LookupEntity<TEnum>, new()
|
|
where TEnum : PrefabEnum<TEnum>
|
|
{
|
|
var dbSet = dbContext.Set<TLookup>();
|
|
|
|
var enumMembers = PrefabEnum<TEnum>.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<TEnum> 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;
|
|
}
|
|
}
|
|
} |