为了账号安全,请及时绑定邮箱和手机立即绑定

为什么 Delta.Patch 不会将字节数组更新为空值?

为什么 Delta.Patch 不会将字节数组更新为空值?

C#
慕无忌1623718 2023-09-24 16:12:20
我的对象模型有一个用于存储图像的字节数组。当我尝试通过Delta.Patch将此字节数组更新为新值时,它工作得很好,但是当我尝试将其更新为 null 时,它失败了。这是我正在查看的代码行update.Patch(entity);如果我查看该update对象,我可以看到其中update.ChangedProperties包含我的图像属性,因此它知道它知道应该更新。我还可以看到它update._instance包含图像字段具有空值的对象实例,并且我可以使用 Fiddler 来查看更改后的值作为空值发送。entity但是,当我在调用后查看对象.Patch时,新值应该为空,字节数组不会更新。Delta 中的其他更新得到正确更新,但此字节数组除外。可能是什么原因造成的?我对 OData 还很陌生,不确定我是否在这里遗漏了一些明显的东西。
查看完整描述

1 回答

?
慕仙森

TA贡献1827条经验 获得超7个赞

我查看了 OData(WebAPI 版本)的源代码并(可能)发现了核心问题。问题也适用于 ASP.NET Core 版本,因为它与 ASP.NET WebAPI 共享代码库。

问题

你调用Patch(TStructuralType original)方法,方法又调用CopyChangedValues(TStructuralType original)方法。两者都是班级的公共成员Delta<T>

public void Patch(TStructuralType original){
    CopyChangedValues(original);
}

内部CopyChangedValues(TStructuralType original)方法是一段处理将值复制到原始实例的代码。代码迭代PropertyAccessor<TStructuralType>数组并调用Copy(TStructuralType from, TStructuralType to)方法。

// For regular non-structural properties at current level.

PropertyAccessor<TStructuralType>[] propertiesToCopy =

                this._changedProperties.Select(s => _allProperties[s]).ToArray();

foreach (PropertyAccessor<TStructuralType> propertyToCopy in propertiesToCopy)

{

    propertyToCopy.Copy(_instance, original);

}

在里面Copy(TStructuralType from, TStructuralType to)实现PropertyAccessor<TStructuralType>你会发现对abstract的调用SetValue(TStructuralType instance, object value)。


public void Copy(TStructuralType from, TStructuralType to)

{

    if (from == null)

    {

        throw Error.ArgumentNull("from");

    }

    if (to == null)

    {

        throw Error.ArgumentNull("to");

    }

    SetValue(to, GetValue(from));

}

这个方法是通过FastPropertyAccessor<TStructuralType>类来实现的。


public override void SetValue(TStructuralType instance, object value)

{

    if (instance == null)

    {

        throw Error.ArgumentNull("instance");

    }


    if (_isCollection)

    {

        DeserializationHelpers.SetCollectionProperty(instance, _property.Name, edmPropertyType: null,

            value: value, clearCollection: true);

    }

    else

    {

        _setter(instance, value);

    }

}

重要的代码行是if (_isCollection). 该布尔标志在构造函数中设置并调用类IsCollection()中的静态方法TypeHelper。


public FastPropertyAccessor(PropertyInfo property)

    : base(property)

{

    _property = property;

    _isCollection = TypeHelper.IsCollection(property.PropertyType);


    if (!_isCollection)

    {

        _setter = PropertyHelper.MakeFastPropertySetter<TStructuralType>(property);

    }

    _getter = PropertyHelper.MakeFastPropertyGetter(property);

}

在IsCollection(Type clrType)我们遍历调用IsCollection(this Type type, out Type elementType).


public static bool IsCollection(Type clrType)

{

    Type elementType;

    return TypeHelper.IsCollection(clrType, out elementType);

}

以下是注释后面的重要几行// see if this type should be ignored.(这很奇怪,可能表明有人忘记完成他已经开始的事情),其中仅排除string( ) 。char[]其他数组(包括byte[])会跳到以下代码,该代码会积极评估 byte[](以及任何其他数组类型),因为这些类型正在实现IEnumerable<T>接口。


public static bool IsCollection(Type clrType, out Type elementType)

{

    if (clrType == null)

    {

        throw Error.ArgumentNull("clrType");

    }


    elementType = clrType;


    // see if this type should be ignored.

    if (clrType == typeof(string))

    {

        return false;

    }


    Type collectionInterface

        = clrType.GetInterfaces()

            .Union(new[] { clrType })

            .FirstOrDefault(

                t => TypeHelper.IsGenericType(t)

                        && t.GetGenericTypeDefinition() == typeof(IEnumerable<>));


    if (collectionInterface != null)

    {

        elementType = collectionInterface.GetGenericArguments().Single();

        return true;

    }


    return false;

}

如果我们跳回方法实现,我们最终会在类中SetValue(TEntityType entity, object value)调用。DeserializationHelpers.SetCollectionProperty(entity, _property.Name, edmPropertyType: null, value: value, clearCollection: true);DeserializationHelpers


if (_isCollection)

{

    DeserializationHelpers.SetCollectionProperty(instance, _property.Name, edmPropertyType: null,

        value: value, clearCollection: true);

}

很明显,此方法的实现非常具有防御性,可以避免在集合值为 时抛出异常null。该方法的第一行是,并且在要执行的代码块之后if (value != null)没有任何块或代码。else我们可以从字面上说,对于每个实现 的类型,空值都会被忽略IEnumerable<T>,因此不会被设置。


internal static void SetCollectionProperty(object resource, string propertyName,

    IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection)

{

    if (value != null)

    {

        IEnumerable collection = value as IEnumerable;

        Contract.Assert(collection != null,

            "SetCollectionProperty is always passed the result of ODataFeedDeserializer or ODataCollectionDeserializer");


        Type resourceType = resource.GetType();

        Type propertyType = GetPropertyType(resource, propertyName);


        Type elementType;

        if (!TypeHelper.IsCollection(propertyType, out elementType))

        {

            string message = Error.Format(SRResources.PropertyIsNotCollection, propertyType.FullName, propertyName, resourceType.FullName);

            throw new SerializationException(message);

        }


        IEnumerable newCollection;

        if (CanSetProperty(resource, propertyName) &&

            CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection))

        {

            // settable collections

            collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType);

            if (propertyType.IsArray)

            {

                newCollection = CollectionDeserializationHelpers.ToArray(newCollection, elementType);

            }


            SetProperty(resource, propertyName, newCollection);

        }

        else

        {

            // get-only collections.

            newCollection = GetProperty(resource, propertyName) as IEnumerable;

            if (newCollection == null)

            {

                string message = Error.Format(SRResources.CannotAddToNullCollection, propertyName, resourceType.FullName);

                throw new SerializationException(message);

            }


            if (clearCollection)

            {

                newCollection.Clear(propertyName, resourceType);

            }


            collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType);

        }

    }

}

解决方案1


第一个可能的解决方案是创建自定义模型绑定程序并处理用于返回空字节数组并向模型绑定程序添加类null的值。byte[]NullByteArrayModelBinder


免责声明:没有测试过,但应该可以。


public class NullByteArrayModelBinder : DefaultModelBinder {

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {

        if(bindingContext.ModelType == typeof(byte[])) {

            return base.BindModel(controllerContext, bindingContext) ?? new byte[0];

        }


        return base.BindModel(controllerContext, bindingContext);

    }

}

这种方法有一个缺点。OData 的使用者还需要在现在进行检查的array.Length > 0任何地方处理空数组。array != null


解决方案2


第二个选项是自定义序列化和反序列化。


序列化:从空array到null=>array.Length > 0 ? array : null;


反序列化:从null到空array=>array ?? new byte[0];


希望能帮助到你!


查看完整回答
反对 回复 2023-09-24
  • 1 回答
  • 0 关注
  • 93 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信