2015-06-16 2 views
19

Hier ist das Szenario. Es gibt einen Web-API-Aufruf, um ein Objekt in der SQL Server-Datenbank zu ändern. Wir wollen nur die Felder auf dem Datenbankobjekt ändern, wenn sie explizit in Webapi Call Json angegeben wurden. Zum Beispiel:WebApi Legen Sie fest, wie nicht spezifizierte Eigenschaften von den angegebenen Eigenschaften auf null gesetzt werden sollen.

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

Das sollte das Feld Name in "newName" und "Color" -Feld auf Null ändern. Im Gegensatz zu diesem json:

, die nur das Feld Name ändern sollte, den alten Wert Farbe intakt lassen.

Was ist ein guter Weg mit WebApi festzustellen, ob eine Eigenschaft übergeben wurde oder nicht?

Wenn ich meine Methode wie folgt definieren:

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

item.Colour wird in beiden Fällen null sein. Beachten Sie, dass ich mit einer Vielzahl von Datentypen hier und Eigentum Colour im Beispiel gerade arbeitete int sein könnte, string, DateTime, Guid usw.

Ich verstehe, dass ich das rohe json mit [FromBody] Attribute erhalten und dann Parsen Sie es selbst, aber es scheint, dass die Standard-Binder bereits die meiste Arbeit (einschließlich Validierung) tut, also wäre ich neugierig, wie ich es wiederverwenden könnte, sondern auch erreichen, was ich will. Was ist der einfachste Weg?

aktualisieren:

ich das meine klären möchte ein „gelegentlich verbunden“ -Szenario. Das heißt, die Geräte, die die API verwenden, sind die meiste Zeit außerhalb der Netzabdeckung und synchronisieren sich von Zeit zu Zeit mit der API.

Praktisch bedeutet dies, dass die größte Teil der Daten, die für die Synchronisierung aggregiert werden in null oder einem „Push-Updates zum Server“ Aufruf von „get neuesten Stand vom Server“ Aufruf gefolgt benötigt wird. Mit Sql Server und EF im Backend, das zu mehreren unterschiedlichen (und manchmal nicht miteinander verwandten) Entitäten führt, sind in einzelnen JSON enthalten. ZB:

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

Auch die Modellklassen, die verwendet werden json für GET Anrufe sind von EF entites getrennt zu erzeugen, wie das Datenbankschema nicht genau das Modell API-Objekt entsprechen.

+0

Um ehrlich zu sein, Put ist idempotent. Du brauchst _patch _... –

+5

@AndreiV, yeah, yeah. Lassen Sie uns nicht darüber diskutieren, was pure Ruhe ist und was nicht =) –

+0

Ich mag es auch nicht ... Als Workaround könnten Sie eine Liste von _changing Eigenschaften_ als zusätzlichen Parameter benötigen. –

Antwort

2

Ich landete mit dynamischer Proxy für die Eigenschaften , damit ich die von JsonMediaTypeFormatter geschriebenen Eigenschaften als "dreckig" markieren konnte. Ich verwendete leicht modifizierte yappi (musste nicht wirklich ändern, wollte nur - erwähnen, wenn der Code unten nicht genau yappi Proben/API entspricht). Ich schätze, Sie können Ihre bevorzugte dynamische Proxy-Bibliothek verwenden. Nur zum Spaß habe ich versucht, es auf NProxy.Core zu portieren, aber das hat nicht funktioniert, weil aus irgendeinem Grund json.net weigerte sich, in Proxies zu schreiben, die NProxy.Core generiert.

So funktioniert es so. Wir haben eine Basisklasse in dieser Richtung:

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 
} 

Irgendwo in der Proxy-Implementierung wir OnPropertyChanged nennen, so dass wir uns daran erinnern, welche Eigenschaften geschrieben wurden.

Dann haben wir unsere eigene 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); 
    } 
} 

Die Idee hier ist, wie JsonMediaTypeFormatter eingehenden json umwandelt, wir das anfängliche leere Objekt ersetzen die Proxy sein, die wir früher definiert.

Wir registrieren diesen Konverter in WebApiConfig.cs wie diese

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

Nun, wenn unser Modell von json anstelle jedes Objekt aus DirtyPropertiesBase dort abgeleitet besiedelt ist, wird ein Proxy mit richtig bevölkert _dirtyList Sammlung. Jetzt müssen wir jedes dieser Modelle nur noch der EF-Entität zuordnen. Das ist einfach genug mit AutoMapper. Wir registrieren jedes Modell wie folgt aus:

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

Und dann haben Sie Ihren gewohnten Mapping-Code:

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

, die dafür sorgen wird, dass nur Schmutzige Eigenschaften aktualisiert werden.

+0

Wo fügen Sie diese Codezeile hinzu? Mapper.CreateMap (). ForAllMembers (x => x.Condition (z => ((Modell) z.Parent.SourceValue) .IsPropertyDirty (z.MemberName))); – cal5barton

+0

@ cal5barton beim Start –

0

Sicher ist dies ein Persistenzproblem, kein Problem mit dem Modellbinder.

Ihre API wird ein Null-Wert für eine bestimmte Eigenschaft zur Verfügung gestellt, damit das Bindemittel es ehrt.

Vielleicht in Persistenz beraten Sie je nachdem, welche Rahmen Sie null Einträge zu ignorieren verwenden (ich nehme an, Sie vorbei bis auf NULL festlegbare int? S statt nur ints)

+0

Nein, das ist nicht der Fall. Auf der Persistenzstufe für einige Felder ist "null" ein gültiger Wert. Es wäre also schön, den Unterschied zwischen einem Nullwert und einem nicht angegebenen Wert zu erkennen. In Ihrem Beispiel funktioniert das Ignorieren des Nulleintrags nicht, da, wenn ein Nullwert mit json bereitgestellt wird, ich * will * es schreiben kann. Ich möchte nur ignorieren, wenn es nicht zur Verfügung gestellt wird. Und wenn json in eine Instanz einer Modellklasse konvertiert wird, ist diese Information über die Anwesenheit bereits verloren. –

1

Während für OData-Dienste eingeführt, können Sie versuchen, System.Web.Http.OData.Delta<T> zu verwenden. Dies ermöglicht teilweise Aktualisierungen von Entitäten.

Werfen Sie einen Blick auf this blog post für eine gute Diskussion über die Verwendung von Delta<T>. Im Wesentlichen läuft es auf der Definition Put und Patch Methoden nach unten, wie:

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

hier eine Anforderung an Put wird das gesamte Modell aktualisiert werden, während eine Anforderung an Patch nur teilweise um das Modell aktualisieren (nur mit den durch die übergebene Eigenschaften Klient).

+0

Ja, ich habe das gesehen. In meinem Fall ist das 'Item' keine EF-Entität und es ist ein komplexer verschachtelter Typ. Ich konnte Delta nicht richtig arbeiten lassen. Insbesondere würde 'model.Patch (entity)' nicht funktionieren, da Modell und Entity unterschiedliche Typen haben. –

+0

Auch alle Modelle innerhalb des äußeren Modells werden überhaupt nicht ausgefüllt. Nur die unmittelbaren einfachen Eigenschaften des äußersten Modells werden ausgefüllt. Alle Modelle der zweiten Ebene enthalten immer 'null'. –

+0

Beispiel: 'class TaskData {public IList AssignedUsers {get; set;} public IList Produkte {get; set;} public Task Aufgabe {get; set}} 'Und dann' public async Aufgabe Put (Delta [] Modell) 'Weder' AssignedUsers' und 'Products' noch' Task' Eigenschaften werden aufgefüllt. –

0

Ich löste das Problem mit diesem Muster.

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

Die Sammelmappe legt die Eigenschaft nicht in beiden Fällen fest, wenn sie null ist und wenn sie nicht angegeben wird. Es hilft also nicht, zwischen diesen beiden Fällen zu unterscheiden. –

+0

Sind Sie sicher? Es hilft mir, zwischen den beiden Fällen zu unterscheiden. Wenn ich {"ID": null} poste, dann ist IDSpecified wahr und wenn ich {} poste, ist es falsch. Verwenden Sie. Net-Core? (Ich bin nicht) –

+1

Natürlich bin ich mir sicher.Ich verbrachte mehrere Tage, vielleicht sogar ein paar Wochen, um dieses spezielle Problem zu lösen und Dinge in verschiedenen Kombinationen auszuprobieren. Ich sage nicht, dass du nicht siehst, was du siehst, ich bin sicher, dass du es bist, aber du hast nicht die gleichen "Gegebenheiten" wie ich. Der Tech-Stack, den ich verwendet habe, und die konkreten Beispiele sind alle in der Frage angegeben. Vielen Dank. –

Verwandte Themen