gandalf-reborn/Data/Database/ApplicationContext.cs
2025-03-02 12:51:02 +01:00

206 lines
8.0 KiB
C#

using Abstractions;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using W542.GandalfReborn.Data.Entities.Base;
using W542.GandalfReborn.Data.Entities.Security;
using W542.GandalfReborn.Data.Entities.Tenant;
using W542.GandalfReborn.Data.Entities.Version;
using W542.GandalfReborn.Data.Extensions;
namespace W542.GandalfReborn.Data.Database;
public sealed class ApplicationContext(DbContextOptions<ApplicationContext> options, InvokerContext invokerContext) : CoreContext<ApplicationContext>(options)
{
private static readonly Dictionary<Type, Type> EntityToVersionEntityMap = new();
private const string Schema = "gr";
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new GrDbConnectionInterceptor(invokerContext));
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
ConfigureId(builder);
AddEnumToStringConversion(builder);
AddVersionRelations(builder);
SetTableNames(builder);
}
private static void ConfigureId(ModelBuilder builder)
{
var longKeyEntities = builder.Model
.GetEntityTypes()
.Where(x => !x.ClrType.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition().IsAssignableTo(typeof(IVersionEntity<>))))
.Where(x => !x.ClrType.GetInterfaces().Any(y => y.IsAssignableTo(typeof(IMappingEntity))))
.Where(x => x.GetProperties().Any(y => y.Name == nameof(IdData.Id) && y.ClrType == typeof(long)))
.ToList();
foreach (var entity in longKeyEntities)
// var idSequenceName = $"{entity.ClrType.Name.Replace("Entity", string.Empty)}_{nameof(IdData.Id)}_{IdSequenceSuffix}";
// builder.HasSequence(idSequenceName).IncrementsBy(100);
builder.Entity(entity.ClrType)
.Property(nameof(IdData.Id))
// .UseIdentityAlwaysColumn();
.ValueGeneratedOnAdd();
// .UseHiLo(idSequenceName)
// .HasColumnType("bigserial");
}
private static void AddVersionRelations(ModelBuilder builder)
{
// var coreTypes = typeof(CoreContext<DbContext>).Assembly
// .GetTypes()
// .Where(x => x is { IsAbstract: false, IsInterface: false } && x.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IVersionEntity<>)));
var dataTypes = typeof(ApplicationContext).Assembly
.GetTypes()
.Where(x => x is { IsAbstract: false, IsInterface: false } && x.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IVersionEntity<>)));
// var allVersionTypes = coreTypes.Concat(dataTypes).ToList();
foreach (var type in dataTypes)
{
var entityBuilder = builder.Entity(type);
var referenceType = type.GetProperties()
.Where(x => x.Name == nameof(IVersionEntity.Reference))
.Select(x => x.PropertyType)
.Single();
EntityToVersionEntityMap.TryAdd(referenceType, type);
var referencePrimaryKeys = builder.Model
.GetEntityTypes()
.Where(x => x.ClrType == referenceType)
.Select(x => x.FindPrimaryKey())
.Single(x => x is not null)!
.Properties
.Select(x => x.Name)
.ToArray();
entityBuilder
.HasKey([..referencePrimaryKeys, nameof(IVersionEntity.At)]);
entityBuilder
.HasOne(nameof(IVersionEntity.Reference))
.WithMany()
.HasForeignKey(referencePrimaryKeys)
.IsRequired();
entityBuilder
.HasOne(nameof(IVersionEntity.Suspect))
.WithMany()
.HasForeignKey(nameof(IVersionEntity.SuspectId))
.IsRequired();
}
var versionTypes = builder.Model
.GetEntityTypes()
.Where(x => x.ClrType is { IsAbstract: false, IsInterface: false } && x.ClrType.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IVersionEntity<>)))
.ToList();
versionTypes.ForEach(type =>
{
var allowedNavigationNames = new List<string>
{
nameof(IVersionEntity.Reference),
nameof(IVersionEntity.Suspect)
};
var navigations = type
.GetNavigations()
.Where(x => !allowedNavigationNames.Contains(x.Name))
.ToList();
var entityBuilder = builder.Entity(type.ClrType);
navigations.ForEach(x => entityBuilder.Ignore(x.Name));
});
}
public void AddVersionTriggers()
{
foreach (var (entityType, versionType) in EntityToVersionEntityMap)
{
var dataType = entityType.BaseType;
if (dataType == null)
throw new Exception($"Could not find base type for {entityType}");
var dataTypeColumns = dataType.GetProperties().Select(x => x.Name).ToList();
var versionTypeColumns = new Dictionary<string, string>()
{
[nameof(IVersionEntity.At)] = "current_timestamp",
[nameof(IVersionEntity.SuspectId)] = $"current_setting('{GrDbConnectionInterceptor.CurrentSuspectKey}', 't')::bigint",
[nameof(IVersionEntity.Action)] = $"(CASE WHEN (tg_op = 'INSERT') THEN '{VersionAction.Created.ToString()}' ELSE '{VersionAction.Modified.ToString()}' END)"
};
var rowColumns = string.Join(", ", dataTypeColumns.Concat(versionTypeColumns.Keys).Select(x => $"\"{x}\""));
var rowValues = string.Join(", ", dataTypeColumns.Select(x => $"NEW.\"{x}\"").Concat(versionTypeColumns.Values));
// Trust me, never change those names.
var triggerName = $"{GetTableName(entityType).ToLower()}_t";
var functionName = $"p_{triggerName}";
var sql = $"""
CREATE OR REPLACE FUNCTION {Schema}.{functionName}()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
insert into {Schema}."{GetTableName(versionType)}" ({rowColumns})
values ({rowValues});
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER {triggerName} AFTER INSERT Or UPDATE ON {Schema}."{GetTableName(entityType)}"
FOR EACH ROW
EXECUTE FUNCTION {Schema}.{functionName}();
""";
#pragma warning disable EF1002
Database.ExecuteSqlRaw(sql);
#pragma warning restore EF1002
}
}
private static void AddEnumToStringConversion(ModelBuilder builder)
{
var entityTypes = builder.Model.GetEntityTypes().ToList();
foreach (var type in entityTypes)
{
var entityBuilder = builder.Entity(type.ClrType);
var propertyInfos = type.ClrType.GetProperties();
foreach (var property in propertyInfos)
if (property.PropertyType.IsEnum)
entityBuilder
.Property(property.Name)
.HasConversion<string>();
}
}
private static void SetTableNames(ModelBuilder builder)
{
var entityTypes = builder.Model
.GetEntityTypes();
foreach (var type in entityTypes)
{
var entityBuilder = builder.Entity(type.ClrType);
var tableName = GetTableName(type.ClrType);
entityBuilder.ToTable(tableName, Schema);
}
}
private static string GetTableName(Type entityType)
{
return $"{entityType.Name.Replace("Entity", string.Empty)}";
}
}