165 lines
6.2 KiB
C#
165 lines
6.2 KiB
C#
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<T> : IPatch<T>
|
|
{
|
|
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<PatchesAttribute>();
|
|
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<PatchMapAttribute>() != 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<PatchMapAttribute>();
|
|
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<TEntity>
|
|
var setMethod = typeof(DbContext)
|
|
.GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
|
|
.MakeGenericMethod(targetEntityType);
|
|
|
|
var dbSet = setMethod.Invoke(_dbContext, null);
|
|
|
|
// Call FindByGeneralIdRefAsync<TEntity>
|
|
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<ForeignKeyAttribute>();
|
|
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);
|
|
}
|
|
}
|
|
} |