首页 > 解决方案 > 为什么 Delta.Patch 不会将字节数组更新为空值?

问题描述

我的对象模型有一个用于存储图像的字节数组。当我尝试通过Delta.Patch将此字节数组更新为新值时,它工作得很好,但是当我尝试将其更新为 null 时,它会失败。

这是我正在查看的代码行

update.Patch(entity);

如果我查看update对象,我可以看到它update.ChangedProperties包含我的 image 属性,因此它知道它知道应该更新它。我还可以看到它update._instance包含对象的实例,该对象的图像字段为空值,我可以使用 Fiddler 来查看更改后的值作为空值发送。

但是当我在新值应该为空时entity调用后查看对象时,字节数组不会更新。.PatchDelta 中的其他更新得到正确更新,但不是这个字节数组。

这可能是什么原因造成的?我还是 OData 的新手,我不确定我是否在这里遗漏了一些明显的东西。

标签: c#.netodata

解决方案


我查看了 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[])跳到下面的代码,当这些类型正在实现IEnumerable<T>接口时,它会积极评估 byte[](和任何其他数组类型)。

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

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

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

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

希望能帮助到你!


推荐阅读