WebApi 把如何从设置为 null 的指定属性中分辨未指定的属性

本文关键字:属性 未指定 null 设置 WebApi | 更新日期: 2023-09-27 17:56:49

这是场景。有一个 Web api put 调用来更改 sql Server 数据库中的对象。我们只想更改数据库对象上的字段,前提是它们在 webapi 调用 json 上显式指定。例如:

{ "Name":"newName", "Colour":null }

这应该将"名称"字段更改为"newName",将"颜色"字段更改为空。与此 json 相反:

{ "Name":"newName" }

这应该只更改"名称"字段,而保留旧的"颜色"值不变。

使用 WebApi 检测属性是否已传递的好方法是什么?

如果我这样定义我的方法:

[HttpPut]
[Route("/item/{id}")]
public void ChangeItem(int id, Item item)
{
    ...
}

无论哪种情况,item.Colour都将为空。请注意,我在这里使用各种数据类型,示例中的属性Colour可以是intstringDateTimeGuid等。

我知道我可以获取带有 [FromBody] 属性的原始 json,然后自己解析它,但似乎默认绑定器已经在完成大部分工作(包括验证),所以我很好奇如何重用它,但也实现我想要的。最简单的方法是什么?

更新

我想澄清一下,我的场景是"偶尔连接"的场景。也就是说,使用 API 的设备大部分时间都在网络覆盖之外,并且它们不时使用 API 进行同步。

实际上,这意味着同步所需的大多数数据都聚合为零个或一个"将更新推送到服务器"调用,然后是"从服务器获取最新状态"调用。在后端使用 Sql Server 和 EF 会导致多个不同的(有时是不相关的)实体包含在单个 json 中。例如:

class TaskData
{ 
    public IList<User> AssignedUsers {get; set;} 
    public IList<Product> Products {get; set;} 
    public Task Task {get; set}
}

此外,用于为 GET 调用生成 json 的模型类与 EF Entites 是分开的,因为数据库架构与 API 对象模型不完全匹配。

WebApi 把如何从设置为 null 的指定属性中分辨未指定的属性

我最终对属性使用了动态代理,以便我可以将JsonMediaTypeFormatter编写的属性标记为"脏"。 我使用了稍微修改过的yappi(实际上不必修改它,只是想修改 - 如果下面的代码不完全匹配yappi示例/API,请提及这一点)。我猜你可以使用你最喜欢的动态代理库。只是为了好玩,我试图将其移植到 NProxy.Core,但这不起作用,因为出于某种原因 json.net 拒绝写入NProxy.Core生成的代理。

所以它是这样工作的。我们有一个基类,大致如下:

public class DirtyPropertiesBase
{
    ...
    // most of these come from Yappi
    public static class Create<TConcept> where TConcept : DirtyPropertiesBase
    {
        public static readonly Type Type =PropertyProxy.ConstructType<TConcept, PropertyMap<TConcept>>(new Type[0], true);
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }
    private readonly List<string> _dirtyList = new List<string>();
    protected void OnPropertyChanged(string name)
    {
        if (!_dirtyList.Contains(name))
        {
            _dirtyList.Add(name);
        }
    }
    public bool IsPropertyDirty(string name)
    {
        return _dirtyList.Contains(name);
    }
    ...
    // some more Yappi specific code that calls OnPropertyChanged
    // when a property setter is called
}

在代理实现中的某个地方,我们调用OnPropertyChanged以便我们记住写入了哪些属性。

然后我们有我们的自定义JsonCreationConverter

class MyJsonCreationConverter : JsonConverter
{
    private static readonly ConcurrentDictionary<Type, Func<DirtyPropertiesBase>> ContructorCache = new ConcurrentDictionary<Type, Func<DirtyPropertiesBase>>();
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("MyJsonCreationConverter should only be used while deserializing.");
    }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        Func<DirtyPropertiesBase> constructor = ContructorCache.GetOrAdd(objectType, x =>
            (Func<DirtyPropertiesBase>)typeof(DirtyPropertiesBase.Create<>).MakeGenericType(objectType).GetField("New").GetValue(null));
        DirtyPropertiesBase value = constructor();
        serializer.Populate(reader, value);
        return value;
    }
    public override bool CanConvert(Type objectType)
    {
        return typeof (DirtyPropertiesBase).IsAssignableFrom(objectType);
    }
}

这里的想法是,JsonMediaTypeFormatter转换传入的 json,我们将初始空对象替换为我们之前定义的代理。

我们在 WebApiConfig 中注册这个转换器.cs像这样

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MyJsonCreationConverter());

现在,当我们的模型从 json 填充而不是从DirtyPropertiesBase派生的每个对象时,将有一个具有正确填充_dirtyList集合的代理。现在,我们只需要将这些模型中的每一个映射回 EF 实体。使用AutoMapper,这很简单。我们像这样注册每个模型:

Mapper.CreateMap<Model, Entity>().ForAllMembers(x => x.Condition(z => ((Model)z.Parent.SourceValue).IsPropertyDirty(z.MemberName)));

然后你有你常用的映射代码:

Entity current = _db.Entity.Single(x => x.Id == Id);
Mapper.Map(update, current);
_db.SaveChanges();

这将确保仅更新脏属性。

虽然引入了 OData 服务,但您可以尝试使用 System.Web.Http.OData.Delta<T> .这允许对实体进行部分更新。

看看这篇博文,对使用Delta<T>进行很好的讨论。从本质上讲,它归结为定义PutPatch方法,例如:

public class MyController : ApiController
{
    // Other actions omitted…
    [AcceptVerbs("Patch")]
    public async Task<IHttpActionResult> Patch(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);
        if (entity == null) {
            return NotFound();
        }
        model.Patch(entity);
        return StatusCode(HttpStatusCode.NoContent);
    }
    public async Task<IHttpActionResult> Put(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);
        if (entity == null) {
            return NotFound();
        }
        model.Put(entity);
        return StatusCode(HttpStatusCode.NoContent);
    }
}

在这里,对Put的请求将更新整个模型,而对Patch的请求只会部分更新模型(仅使用客户端传递的属性)。

当然,这是一个持久性问题,而不是模型绑定器问题。

您的 API 正在为给定属性提供空值,因此绑定程序将遵循该值。

也许在持久性中,您可以建议您使用的任何框架忽略空条目(我假设您正在传递可空的 int 而不仅仅是 int)

我使用这种模式解决了这个问题。

public class ValuesController : ApiController
{
    public void Put(int id, [FromBody]Item value)
    {
        if (value.NameSpecified)
        {
        }
        else
        {
        }
    }
}
public class Item
{
    internal bool NameSpecified = false;
    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            NameSpecified = true;
        }
    }
}