You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core批量Patch更新属性值类型转换失败问题

批量更新接口的类型转换与属性验证问题

我在ASP.NET Core应用中开发批量更新接口,接口接收PatchObject集合并将变更应用到对应实体,但调用patch.TrySetPropertyValue时频繁出现类型转换失败的错误,错误提示为「无法将值设置到属性」。

现有代码

[HttpPost("PatchBatch")]
public virtual async Task<IActionResult> PatchBatch([FromBody] ICollection<PatchObject> patchRequests)
{
    if (patchRequests == null || !patchRequests.Any())
    {
        return StatusCode(500, "Cannot complete operation, collection parameter is null or empty");
    }

    var errors = new List<string>();

    foreach (var patchRequest in patchRequests)
    {
        try
        {
            var currentEntity = await _service.GetByIdAsync(patchRequest.Id).SingleOrDefaultAsync();
            
            if (currentEntity == null)
            {
                errors.Add($"Entity with id {patchRequest.Id} not found.");
                continue;
            }

            var patch = new Delta<T>();
            foreach (var change in patchRequest.Changes)
            {
                try
                {
                    if (!patch.TrySetPropertyValue(change.Key, change.Value))
                    {
                        errors.Add($"The value {change.Value} cannot be set to property {change.Key}");
                        continue;
                    }
                }
                catch (ArgumentException)
                {
                    errors.Add($"Property {change.Key} does not exist on the entity.");
                    continue;
                }
            }

            patch.Patch(currentEntity);
            await _service.UpdateAsync(currentEntity);
        }
        catch (Exception ex)
        {
            errors.Add($"An error occurred while updating the entity with id: {patchRequest.Id}. Exception: {ex.Message}");
        }
    }

    if (errors.Any())
    {
        return BadRequest(string.Join("\n", errors));
    }

    return Ok("Batch update successful.");
}

public class PatchObject
{
    public int Id { get; set; }
    public Dictionary<string, object> Changes { get; set; }
}

尝试的转换代码(仍有错误)

PropertyInfo propertyInfo = typeof(T).GetProperty(change.Key.ToString());
if (propertyInfo == null)
{
    errors.Add($"Property {change.Key} does not exist on T.");
    continue;
}

Type propertyType = propertyInfo.PropertyType;
object convertedValue = ConvertToType(change.Value, propertyType);

if (!patch.TrySetPropertyValue(change.Key, convertedValue))
{
    errors.Add($"The value {change.Value} cannot be converted to {propertyType}");
    continue;
}

咨询问题

  1. 如何在此场景下正确处理类型转换?
  2. 如何确保实体上存在对应属性且类型匹配?
  3. 是否有更高效的批量更新方法可避免此类问题?

解决方案

1. 正确处理类型转换

自定义转换逻辑需覆盖可空类型、枚举、基础类型等场景,以下是通用实现:

private object ConvertToType(object value, Type targetType)
{
    if (value == null)
    {
        // 处理可空类型
        if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
            return null;
        // 非可空类型传入null时返回默认值
        return Activator.CreateInstance(targetType);
    }

    // 直接赋值兼容的类型
    if (targetType.IsAssignableFrom(value.GetType()))
        return value;

    // 枚举类型转换
    if (targetType.IsEnum)
    {
        if (value is string strValue)
            return Enum.Parse(targetType, strValue, ignoreCase: true);
        if (value is int intValue)
            return Enum.ToObject(targetType, intValue);
    }

    // 可空类型的基础类型转换
    var underlyingType = Nullable.GetUnderlyingType(targetType);
    if (underlyingType != null)
        return Convert.ChangeType(value, underlyingType);

    // 通用类型转换
    try
    {
        return Convert.ChangeType(value, targetType);
    }
    catch (InvalidCastException)
    {
        throw new ArgumentException($"无法将值 {value} 转换为 {targetType.Name} 类型");
    }
}

使用时先完成转换再调用TrySetPropertyValue:

foreach (var change in patchRequest.Changes)
{
    try
    {
        var propertyInfo = typeof(T).GetProperty(change.Key);
        if (propertyInfo == null)
        {
            errors.Add($"实体不存在属性 {change.Key}");
            continue;
        }

        object convertedValue;
        try
        {
            convertedValue = ConvertToType(change.Value, propertyInfo.PropertyType);
        }
        catch (ArgumentException ex)
        {
            errors.Add(ex.Message);
            continue;
        }

        if (!patch.TrySetPropertyValue(change.Key, convertedValue))
        {
            errors.Add($"无法将值 {convertedValue} 设置到属性 {change.Key}");
            continue;
        }
    }
    catch (Exception ex)
    {
        errors.Add($"处理属性 {change.Key} 时出错:{ex.Message}");
        continue;
    }
}

2. 验证属性存在与类型匹配

通过静态缓存实体属性提升性能,同时添加完整的类型兼容性验证:

// 静态缓存实体属性,避免重复反射
private static readonly Dictionary<Type, PropertyInfo[]> _entityProperties = new Dictionary<Type, PropertyInfo[]>();

private PropertyInfo GetEntityProperty(string propertyName)
{
    var entityType = typeof(T);
    if (!_entityProperties.TryGetValue(entityType, out var properties))
    {
        properties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        _entityProperties[entityType] = properties;
    }
    return properties.FirstOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
}

// 验证类型兼容性
private bool IsTypeCompatible(Type sourceType, Type targetType)
{
    var targetUnderlying = Nullable.GetUnderlyingType(targetType);
    if (targetUnderlying != null)
    {
        return targetUnderlying.IsAssignableFrom(sourceType) || 
               (sourceType == typeof(string) && string.IsNullOrEmpty((string)change.Value));
    }

    return targetType.IsAssignableFrom(sourceType) ||
           (targetType.IsEnum && sourceType.IsNumericType()) ||
           (sourceType == typeof(string) && targetType.IsEnum);
}

// 扩展方法:判断是否为数值类型
public static bool IsNumericType(this Type type)
{
    type = Nullable.GetUnderlyingType(type) ?? type;
    return type == typeof(int) || type == typeof(long) || type == typeof(decimal) || 
           type == typeof(float) || type == typeof(double) || type == typeof(byte);
}

处理变更时的验证逻辑:

var propertyInfo = GetEntityProperty(change.Key);
if (propertyInfo == null)
{
    errors.Add($"实体 {typeof(T).Name} 不存在属性 '{change.Key}'");
    continue;
}

var valueType = change.Value?.GetType();
if (valueType != null && !IsTypeCompatible(valueType, propertyInfo.PropertyType))
{
    errors.Add($"值类型 {valueType.Name} 与属性 {propertyInfo.Name}(类型:{propertyInfo.PropertyType.Name})不兼容");
    continue;
}

// 执行转换与赋值...

3. 更高效的批量更新方案

当前逐个加载实体的方案性能较低,推荐以下两种优化方式:

方案一:EF Core直接生成批量更新SQL

使用ExecuteUpdate避免加载整个实体,直接执行更新操作:

foreach (var patchRequest in patchRequests)
{
    try
    {
        var updateBuilder = _service.GetQueryable().Where(e => e.Id == patchRequest.Id).ExecuteUpdateBuilder();
        
        foreach (var change in patchRequest.Changes)
        {
            var propertyInfo = GetEntityProperty(change.Key);
            if (propertyInfo == null)
            {
                errors.Add($"实体 {typeof(T).Name} 不存在属性 '{change.Key}'");
                continue;
            }

            object convertedValue;
            try
            {
                convertedValue = ConvertToType(change.Value, propertyInfo.PropertyType);
            }
            catch (ArgumentException ex)
            {
                errors.Add(ex.Message);
                continue;
            }

            updateBuilder.SetProperty(propertyInfo, convertedValue);
        }

        await updateBuilder.ExecuteAsync();
    }
    catch (Exception ex)
    {
        errors.Add($"更新实体 {patchRequest.Id} 失败:{ex.Message}");
        continue;
    }
}

方案二:使用强类型DTO自动验证

定义对应实体的PatchDTO,利用ASP.NET Core模型绑定自动完成类型转换与验证:

// 示例:针对Product实体的PatchDTO
public class ProductPatchDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public decimal? Price { get; set; }
    public int? Stock { get; set; }
}

// 修改接口接收DTO集合
[HttpPost("PatchBatch")]
public virtual async Task<IActionResult> PatchBatch([FromBody] ICollection<ProductPatchDTO> patchRequests)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    
    // 直接使用DTO中的强类型值进行更新
    foreach (var dto in patchRequests)
    {
        var entity = await _service.GetByIdAsync(dto.Id).SingleOrDefaultAsync();
        if (entity == null)
        {
            errors.Add($"实体 {dto.Id} 不存在");
            continue;
        }
        
        if (dto.Name != null) entity.Name = dto.Name;
        if (dto.Price.HasValue) entity.Price = dto.Price.Value;
        if (dto.Stock.HasValue) entity.Stock = dto.Stock.Value;
        
        await _service.UpdateAsync(entity);
    }
}

这种方式无需手动处理类型转换,模型绑定会自动验证输入格式,错误直接通过ModelState返回。


内容的提问来源于stack exchange,提问作者EL GHAZAL Said

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 22:32:10