【问题标题】:WebApi Put how to tell not specified properties from specified properties set to null?WebApi Put如何从指定的属性中告诉未指定的属性设置为null?
【发布时间】:2015-09-01 20:23:05
【问题描述】:

这里是场景。有一个 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 实体是分开的,因为数据库架构与 API 对象模型不完全匹配。

【问题讨论】:

  • 说实话,put是幂等的。你需要 patch...
  • @AndreiV,是的,是的。让我们不要争论什么是纯粹的休息,什么不是=)
  • 我也不喜欢...作为一种解决方法,您可能需要一个更改属性列表作为附加参数。
  • @AndreiV 是的。现在就是这样。还是很尴尬。我的 api 消费者问我为什么需要这个列表,如果我能说出我需要的所有表单 json...
  • @YuvalItzchakov,你知道吗。这可能只是可行的。虽然我们需要就每种类型的值达成一致(如int.MaxValuenew Guid("ffffffff-ffff-ffff-ffff-ffffffffffff")DateTime.MaxValue 等。我会考虑的。)谢谢你的建议。

标签: c# .net json asp.net-web-api


【解决方案1】:

我最终为属性使用了动态代理,这样我就可以将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();

这将确保只更新脏属性。

【讨论】:

  • 这行代码你在哪里添加? Mapper.CreateMap().ForAllMembers(x => x.Condition(z => ((Model)z.Parent.SourceValue).IsPropertyDirty(z.MemberName)));
  • @cal5barton 在启动时
【解决方案2】:

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

查看this blog post,了解有关使用Delta&lt;T&gt; 的良好讨论。本质上,它归结为定义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 的请求将仅更新模型的一部分(仅使用客户端传递的属性)。

【讨论】:

  • 是的,我见过这个。在我的情况下,Item 不是 EF 实体,它是一个复杂的嵌套类型。我无法让Delta 正常工作。特别是model.Patch(entity) 不起作用,因为模型和实体属于不同类型。
  • 此外,外部模型内的所有模型根本不会被填充。仅填充最外层模型的直接简单属性。所有二级模型始终包含null
  • 例如:class TaskData{ public IList&lt;User&gt; AssignedUsers {get; set;} public IList&lt;Product&gt; Products {get; set;} public Task Task {get; set}} 然后是 public async Task&lt;IHttpActionResult&gt; Put(Delta&lt;TaskData&gt;[] model) AssignedUsersProductsTask 属性都不会被填充。
  • @zespri 好的,感谢您提供更多信息。但是,我很惊讶Delta&lt;T&gt; 不适用于嵌套属性。无论如何,您的问题并不清楚您需要支持嵌套属性,例如您之前评论中的示例(您问题中的 Item 对象仅显示两个原始类型属性)。也许您可以更新您的问题以包含更完整的示例。我将把这个问题留给可能只需要支持简单(即非嵌套)模型的其他用户。
  • 谢谢。我以为我用With Sql Server and EF in the back-end that leads to several different (and sometimes unrelated) entities are contained within single json. 表达了这个想法,也许这样的一个例子会引起对这个声明的更多关注。让我添加它。
【解决方案3】:

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

您的 API 为给定属性提供了一个空值,因此活页夹正在兑现它。

也许在持久性中,您可以建议您使用哪个框架来忽略空条目(我假设您传递的是可为空的 int?s 而不仅仅是 int)

【讨论】:

  • 不,事实并非如此。在某些字段的持久性级别上,null 是一个有效值。因此,最好能区分空值和未提供的值。在您的示例中,忽略 null 条目将不起作用,因为如果 json 提供了 null 值,我 想要 编写它。如果没有提供,我只想忽略它。而当 json 被转换为模型类的实例时,这个关于存在的信息已经丢失了。
【解决方案4】:

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

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;
        }
    }
}

【讨论】:

  • 在属性为 null 和未指定的两种情况下,活页夹都不会设置属性。因此,这无助于区分这两种情况。
  • 你确定吗?它帮助我区分这两种情况。如果我发布 { "ID": null } 则 IDSpecified 为真,如果我发布 { } 则为假。您使用的是 .net 核心吗? (我不是)
  • 我当然确定。我花了几天甚至几周的时间来解决这个特定的问题并尝试不同的组合。我并不是说你没有看到你所看到的,我相信你是,但你没有我所拥有的相同的“给定”。我使用的技术堆栈和具体示例都在问题中给出。谢谢。
  • 告诉您,您如何对您的案例进行最小的复制,将其上传到 github 并在此处发布链接。然后我就可以看看,看看有什么不同。我可以确认我没有使用 .net core。
  • 我只在web api项目中使用了上面的代码(我使用的是vs2017),新建一个web api项目并将该代码粘贴到valuecontroller中并尝试一下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 1970-01-01
  • 2012-12-17
  • 2012-10-06
相关资源
最近更新 更多