using e_suite.API.Common.exceptions; using e_suite.Database.Core; using e_suite.Database.Core.Extensions; using e_suite.Database.Core.Models; using eSuite.Core.Miscellaneous; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; namespace e_suite.API.Common; public class Patch : IPatch { private readonly IEsuiteDatabaseDbContext _dbContext; private readonly Type _targetType; public T Value { get; } public Patch(T value, IEsuiteDatabaseDbContext dbContext) { Value = value; _dbContext = dbContext; var patchesAttr = typeof(T).GetCustomAttribute(); if (patchesAttr == null) throw new InvalidDataException( $"Patch DTO '{typeof(T).Name}' is missing required [Patches] attribute."); _targetType = patchesAttr.TargetType; } public async Task ApplyTo(object target) { if (target == null) throw new ArgumentNullException(nameof(target)); if (!_targetType.IsAssignableFrom(target.GetType())) { throw new InvalidDataException( $"Patch DTO '{typeof(T).Name}' is defined to patch '{_targetType.Name}', " + $"but was applied to instance of '{target.GetType().Name}'."); } var targetType = target.GetType(); var targetProps = targetType .GetProperties(BindingFlags.Public | BindingFlags.Instance) .ToDictionary(p => p.Name); var patchProps = typeof(T) .GetProperties(BindingFlags.Public | BindingFlags.Instance); var mappedProps = patchProps .Where(p => p.GetCustomAttribute() != null) .ToList(); if (mappedProps.Count == 0) { throw new InvalidDataException( $"Patch DTO '{typeof(T).Name}' does not define any properties with PatchMapAttribute."); } foreach (var patchProp in patchProps) { // Only process properties with PatchMap var mapAttr = patchProp.GetCustomAttribute(); if (mapAttr == null) continue; var targetPropName = mapAttr.TargetPropertyName; if (!targetProps.TryGetValue(targetPropName, out var targetProp)) throw new InvalidDataException( $"PatchMap refers to unknown property '{targetPropName}' on '{targetType.Name}'"); if (!targetProp.CanWrite) throw new InvalidDataException( $"Target property '{targetPropName}' is not writable"); var patchValue = patchProp.GetValue(Value); // Null means "not included in the patch" if (patchValue == null) continue; bool isGeneralIdRef = typeof(IGeneralIdRef).IsAssignableFrom(patchProp.PropertyType); // Disallow nested objects (except string and IGeneralIdRef) if (patchProp.PropertyType.IsClass && patchProp.PropertyType != typeof(string) && !isGeneralIdRef) { throw new InvalidDataException( $"Nested objects are not allowed in PATCH DTOs. Property: {patchProp.Name}"); } // Handle navigation properties via IGeneralIdRef if (isGeneralIdRef) { // Ensure the target navigation type implements IGeneralId if (!typeof(IGeneralId).IsAssignableFrom(targetProp.PropertyType)) { throw new InvalidDataException( $"PatchMap for '{patchProp.Name}' refers to navigation property '{targetPropName}', " + $"but '{targetProp.PropertyType.Name}' does not implement IGeneralId. " + $"Navigation properties patched via IGeneralIdRef must implement IGeneralId."); } var refValue = (IGeneralIdRef)patchValue; var targetEntityType = targetProp.PropertyType; // Get DbSet var setMethod = typeof(DbContext) .GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! .MakeGenericMethod(targetEntityType); var dbSet = setMethod.Invoke(_dbContext, null); // Call FindByGeneralIdRefAsync var findMethod = typeof(QueryableExtensions) .GetMethod(nameof(QueryableExtensions.FindByGeneralIdRefAsync))! .MakeGenericMethod(targetEntityType); var task = (Task)findMethod.Invoke( null, [dbSet, refValue, CancellationToken.None] )!; await task.ConfigureAwait(false); var resultProperty = task.GetType().GetProperty("Result"); var resolvedEntity = resultProperty?.GetValue(task); if (resolvedEntity == null) throw new NotFoundException( $"Unable to resolve reference for '{patchProp.Name}'"); // Assign navigation property targetProp.SetValue(target, resolvedEntity); // Look for ForeignKeyAttribute on the navigation property var fkAttr = targetProp.GetCustomAttribute(); if (fkAttr != null) { var fkName = fkAttr.Name; if (targetProps.TryGetValue(fkName, out var fkProp)) { var idValue = resolvedEntity .GetType() .GetProperty("Id") ?.GetValue(resolvedEntity); fkProp.SetValue(target, idValue); } } continue; } // Default scalar assignment targetProp.SetValue(target, patchValue); } } }