简体   繁体   English

WebApi如何将指定属性的指定属性设置为null?

[英]WebApi Put how to tell not specified properties from specified properties set to null?

Here is the scenario. 这是场景。 There is an web api put call to change an object in sql server database. 有一个web api put调用来更改sql server数据库中的对象。 We want only to change the fields on the database object if they were explicitly specified on webapi call json. 我们只想更改数据库对象上的字段,如果它们是在webapi调用json上明确指定的。 For example: 例如:

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

That should change the Name field to "newName" and "Colour" field to null. 这应该将Name字段更改为“newName”和“Color”字段为null。 As opposed to this json: 与这个json相反:

{ "Name":"newName" }

that should only change the Name field, leaving the old Colour value intact. 应该只更改名称字段,保持旧的颜色值不变。

What is a good way with WebApi to detect whether a property was passed or not? WebApi检测属性是否通过的好方法是什么?

If I define my method like this: 如果我像这样定义我的方法:

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

item.Colour will be null in either case. item.Colour在任何一种情况下都将为null。 Note, that I'm working with a variety of data types here and property Colour in the example could be int , string , DateTime , Guid , etc. 注意,我在这里使用各种数据类型和属性示例中的Colour可以是intstringDateTimeGuid等。

I understand that I can get the raw json with [FromBody] attribute and then parse it myself, but it seems that the default binder is already doing most of the work (including validation), so I would be curious how I could reuse it, but also achieve what I want. 我知道我可以使用[FromBody]属性获取原始json,然后自己解析它,但似乎默认绑定器已经完成了大部分工作(包括验证),所以我很好奇我如何重用它,但也实现了我想要的。 What is the easiest way? 什么是最简单的方法?

Update : 更新

I'd like to clarify that mine is an "occasionally connected" scenario. 我想澄清一下,我的是“偶尔联系”的情景。 That is, the devices that are using the API are out of network coverage most of the time, and they sync using the API from time to time. 也就是说,使用API​​的设备大多数时间都不在网络覆盖范围内,并且它们会不时使用API​​进行同步。

Practically this means that most of the data that is needed to sync are aggregated into zero or one "push updates to server" call followed by "get latest state from server" call. 实际上,这意味着同步所需的大多数数据被聚合为零或一个“推送更新到服务器”调用,然后是“从服务器获取最新状态”调用。 With Sql Server and EF in the back-end that leads to several different (and sometimes unrelated) entities are contained within single json. 使用后端的Sql Server和EF导致几个不同的(有时是无关的)实体包含在单个json中。 Eg: 例如:

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

Also the model classes that are used to generate json for GET calls are separate from EF Entites, as the database schema does not exactly match the API object model. 此外,用于为GET调用生成json的模型类与EF Entites是分开的,因为数据库模式与API对象模型不完全匹配。

I ended up using dynamic proxy for the properties, so that I could mark the properties written by JsonMediaTypeFormatter as "dirty". 我最终使用动态代理来处理属性,这样我就可以将JsonMediaTypeFormatter写的属性标记为“脏”。 I used slightly modified yappi (did not really have to modify it, just wanted to - mentioning this if the code below does not exactly match yappi samples/API). 我使用了稍微修改过的yappi (没有真正修改它,只是想 - 如果下面的代码与yappi samples / API不完全匹配,请提及此内容)。 I'm guessing you can use your favourite dynamic proxy library. 我猜你可以使用自己喜欢的动态代理库。 Just for fun I tried to port it to NProxy.Core but that did not work because for some reason json.net refused to write into proxies that NProxy.Core generated. 只是为了好玩,我试图将它移植到NProxy.Core,但这不起作用,因为由于某种原因json.net拒绝写入NProxy.Core生成的代理。

So it works like this. 所以它就是这样的。 We have a base class along these lines: 我们有这样一个基类:

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
}

Somewhere in the proxy implementation we call OnPropertyChanged so that we remember which properties were written to. 在代理实现的某个地方,我们调用OnPropertyChanged以便我们记住写入了哪些属性。

Then we have our custom JsonCreationConverter : 然后我们有自定义的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);
    }
}

The idea here, is as JsonMediaTypeFormatter converts incoming json, we substitute the initial empty object to be the proxy that we defined earlier. 这里的想法是,当JsonMediaTypeFormatter转换传入的json时,我们将初始的空对象替换为我们之前定义的代理。

We register this converter in WebApiConfig.cs like this 我们在WebApiConfig.cs中注册这个转换器

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

Now when our model is populated from json in place of each object derived from DirtyPropertiesBase there will be a proxy with properly populated _dirtyList collection. 现在,当我们的模型从json填充而不是从DirtyPropertiesBase派生的每个对象时,将会有一个具有正确填充的_dirtyList集合的代理。 Now we only need to map each of these models back to EF entity. 现在我们只需要将每个模型映射回EF实体。 This is simple enough with AutoMapper . 使用AutoMapper这很简单。 We register each model like this: 我们注册每个模型:

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

And then you have your usual mapping code: 然后你有你通常的映射代码:

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

That will make sure that only Dirty properties are updated. 这将确保只更新Dirty属性。

Whilst introduced for OData services, you could try using System.Web.Http.OData.Delta<T> . 虽然为OData服务引入,但您可以尝试使用System.Web.Http.OData.Delta<T> This allows for partial updates of entities. 这允许实体的部分更新。

Take a look at this blog post for a good discussion on using Delta<T> . 看一下这篇博客文章 ,就使用Delta<T>进行讨论。 Essentially it boils down to defining Put and Patch methods such as: 从本质上讲,它归结为定义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);
    }
}

Here a request to Put will update the entire model, whereas a request to Patch will only partially update the model (using only the properties passed by the client). 这里对Put的请求将更新整个模型,而对Patch的请求将仅部分更新模型(仅使用客户端传递的属性)。

Surely this is a persistence issue, not a model binder issue. 当然这是一个持久性问题,而不是模型绑定器问题。

Your API is being provided a null value for a given property, therefore the binder is honouring it. 您的API为给定属性提供了空值,因此绑定器正在遵循它。

Perhaps in persistence you can advise whichever framework you're using to ignore null entries (I assume you're passing up nullable int?s instead of just ints) 也许在持久性中,您可以建议您使用哪个框架来忽略空条目(我假设您传递的是可空的int而不仅仅是int)

I solved the problem by using this pattern. 我通过使用这种模式解决了这个问题。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM