Init
This commit is contained in:
199
Prefab/Data/Seeder/PrefabDbSeeder.cs
Normal file
199
Prefab/Data/Seeder/PrefabDbSeeder.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user